@backstage/plugin-catalog-backend 1.5.1 → 1.6.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,35 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.6.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 16891a212c: Added new `POST /entities/by-refs` endpoint, which allows you to efficiently
8
+ batch-fetch entities by their entity ref. This can be useful e.g. in graphql
9
+ resolvers or similar contexts where you need to fetch many entities at the same
10
+ time.
11
+
12
+ ### Patch Changes
13
+
14
+ - d8593ce0e6: Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package
15
+ - 3280711113: Updated dependency `msw` to `^0.49.0`.
16
+ - e982f77fe3: Registered shutdown hook in experimental catalog plugin.
17
+ - Updated dependencies
18
+ - @backstage/catalog-client@1.2.0-next.0
19
+ - @backstage/backend-common@0.16.1-next.0
20
+ - @backstage/integration@1.4.1-next.0
21
+ - @backstage/plugin-permission-common@0.7.2-next.0
22
+ - @backstage/plugin-permission-node@0.7.2-next.0
23
+ - @backstage/types@1.0.2-next.0
24
+ - @backstage/backend-plugin-api@0.1.5-next.0
25
+ - @backstage/plugin-catalog-node@1.2.2-next.0
26
+ - @backstage/catalog-model@1.1.4-next.0
27
+ - @backstage/config@1.0.5-next.0
28
+ - @backstage/errors@1.1.4-next.0
29
+ - @backstage/plugin-catalog-common@1.0.9-next.0
30
+ - @backstage/plugin-scaffolder-common@1.2.3-next.0
31
+ - @backstage/plugin-search-common@1.1.2-next.0
32
+
3
33
  ## 1.5.1
4
34
 
5
35
  ### 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.0",
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
  /**
@@ -427,7 +427,7 @@ export declare class CodeOwnersProcessor implements CatalogProcessor {
427
427
  reader: UrlReader;
428
428
  });
429
429
  getProcessorName(): string;
430
- preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
430
+ preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
431
431
  }
432
432
 
433
433
  /**
@@ -586,7 +586,7 @@ export { EntityRelationSpec }
586
586
  /** @public */
587
587
  export declare class FileReaderProcessor implements CatalogProcessor {
588
588
  getProcessorName(): string;
589
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
589
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
590
590
  }
591
591
 
592
592
  /** @public */
@@ -605,7 +605,7 @@ export declare class LocationEntityProcessor implements CatalogProcessor {
605
605
  private readonly options;
606
606
  constructor(options: LocationEntityProcessorOptions);
607
607
  getProcessorName(): string;
608
- postProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
608
+ postProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
609
609
  }
610
610
 
611
611
  /** @public */
@@ -613,16 +613,28 @@ export declare type LocationEntityProcessorOptions = {
613
613
  integrations: ScmIntegrationRegistry;
614
614
  };
615
615
 
616
- export { LocationSpec }
616
+ /**
617
+ * Holds the entity location information.
618
+ *
619
+ * @remarks
620
+ *
621
+ * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch.
622
+ * This flag is then set to indicate that the file can be not present.
623
+ * default value: 'required'.
624
+ *
625
+ * @public
626
+ * @deprecated use the same type from `@backstage/plugin-catalog-common` instead
627
+ */
628
+ export declare type LocationSpec = LocationSpec_2;
617
629
 
618
630
  /** @public */
619
631
  export declare function locationSpecToLocationEntity(opts: {
620
- location: LocationSpec;
632
+ location: LocationSpec_2;
621
633
  parentEntity?: Entity;
622
634
  }): LocationEntityV1alpha1;
623
635
 
624
636
  /** @public */
625
- export declare function parseEntityYaml(data: Buffer, location: LocationSpec): Iterable<CatalogProcessorResult>;
637
+ export declare function parseEntityYaml(data: Buffer, location: LocationSpec_2): Iterable<CatalogProcessorResult>;
626
638
 
627
639
  /**
628
640
  * These permission rules can be used to conditionally filter catalog entities
@@ -663,7 +675,7 @@ export declare class PlaceholderProcessor implements CatalogProcessor {
663
675
  private readonly options;
664
676
  constructor(options: PlaceholderProcessorOptions);
665
677
  getProcessorName(): string;
666
- preProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
678
+ preProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
667
679
  }
668
680
 
669
681
  /** @public */
@@ -719,7 +731,7 @@ export declare class UrlReaderProcessor implements CatalogProcessor {
719
731
  logger: Logger;
720
732
  });
721
733
  getProcessorName(): string;
722
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
734
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
723
735
  private doRead;
724
736
  }
725
737
 
@@ -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
  /**
@@ -380,7 +380,7 @@ export declare class CodeOwnersProcessor implements CatalogProcessor {
380
380
  reader: UrlReader;
381
381
  });
382
382
  getProcessorName(): string;
383
- preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
383
+ preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
384
384
  }
385
385
 
386
386
  /* Excluded from this release type: createCatalogConditionalDecision */
@@ -501,7 +501,7 @@ export { EntityRelationSpec }
501
501
  /** @public */
502
502
  export declare class FileReaderProcessor implements CatalogProcessor {
503
503
  getProcessorName(): string;
504
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
504
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
505
505
  }
506
506
 
507
507
  /** @public */
@@ -520,7 +520,7 @@ export declare class LocationEntityProcessor implements CatalogProcessor {
520
520
  private readonly options;
521
521
  constructor(options: LocationEntityProcessorOptions);
522
522
  getProcessorName(): string;
523
- postProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
523
+ postProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
524
524
  }
525
525
 
526
526
  /** @public */
@@ -528,16 +528,28 @@ export declare type LocationEntityProcessorOptions = {
528
528
  integrations: ScmIntegrationRegistry;
529
529
  };
530
530
 
531
- export { LocationSpec }
531
+ /**
532
+ * Holds the entity location information.
533
+ *
534
+ * @remarks
535
+ *
536
+ * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch.
537
+ * This flag is then set to indicate that the file can be not present.
538
+ * default value: 'required'.
539
+ *
540
+ * @public
541
+ * @deprecated use the same type from `@backstage/plugin-catalog-common` instead
542
+ */
543
+ export declare type LocationSpec = LocationSpec_2;
532
544
 
533
545
  /** @public */
534
546
  export declare function locationSpecToLocationEntity(opts: {
535
- location: LocationSpec;
547
+ location: LocationSpec_2;
536
548
  parentEntity?: Entity;
537
549
  }): LocationEntityV1alpha1;
538
550
 
539
551
  /** @public */
540
- export declare function parseEntityYaml(data: Buffer, location: LocationSpec): Iterable<CatalogProcessorResult>;
552
+ export declare function parseEntityYaml(data: Buffer, location: LocationSpec_2): Iterable<CatalogProcessorResult>;
541
553
 
542
554
  /* Excluded from this release type: permissionRules */
543
555
 
@@ -550,7 +562,7 @@ export declare class PlaceholderProcessor implements CatalogProcessor {
550
562
  private readonly options;
551
563
  constructor(options: PlaceholderProcessorOptions);
552
564
  getProcessorName(): string;
553
- preProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
565
+ preProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
554
566
  }
555
567
 
556
568
  /** @public */
@@ -606,7 +618,7 @@ export declare class UrlReaderProcessor implements CatalogProcessor {
606
618
  logger: Logger;
607
619
  });
608
620
  getProcessorName(): string;
609
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
621
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
610
622
  private doRead;
611
623
  }
612
624
 
package/dist/index.cjs.js CHANGED
@@ -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",
@@ -3230,6 +3255,19 @@ class Stitcher {
3230
3255
  }
3231
3256
  }
3232
3257
 
3258
+ const schema = zod.z.object({
3259
+ entityRefs: zod.z.array(zod.z.string())
3260
+ });
3261
+ function entitiesBatchRequest(req) {
3262
+ try {
3263
+ return schema.parse(req.body);
3264
+ } catch (error) {
3265
+ throw new errors.InputError(
3266
+ `Malformed request body (did you remember to specify an application/json content type?), ${error.message}`
3267
+ );
3268
+ }
3269
+ }
3270
+
3233
3271
  function basicEntityFilter(items) {
3234
3272
  const filtersByKey = {};
3235
3273
  for (const [key, value] of Object.entries(items)) {
@@ -3369,6 +3407,17 @@ function parseEntityTransformParams(params) {
3369
3407
  };
3370
3408
  }
3371
3409
 
3410
+ function parseEntityFacetParams(params) {
3411
+ const facetStrings = parseStringsParam(params.facet, "facet");
3412
+ if (facetStrings) {
3413
+ const filtered = facetStrings.filter(Boolean);
3414
+ if (filtered.length) {
3415
+ return filtered;
3416
+ }
3417
+ }
3418
+ throw new errors.InputError("Missing facet parameter");
3419
+ }
3420
+
3372
3421
  async function requireRequestBody(req) {
3373
3422
  const contentType = req.header("content-type");
3374
3423
  if (!contentType) {
@@ -3405,17 +3454,6 @@ function disallowReadonlyMode(readonly) {
3405
3454
  }
3406
3455
  }
3407
3456
 
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
3457
  async function createRouter(options) {
3420
3458
  const {
3421
3459
  entitiesCatalog,
@@ -3503,7 +3541,16 @@ async function createRouter(options) {
3503
3541
  });
3504
3542
  res.status(200).json(response);
3505
3543
  }
3506
- ).get("/entity-facets", async (req, res) => {
3544
+ ).post("/entities/by-refs", async (req, res) => {
3545
+ const request = entitiesBatchRequest(req);
3546
+ const token = getBearerToken(req.header("authorization"));
3547
+ const response = await entitiesCatalog.entitiesBatch({
3548
+ entityRefs: request.entityRefs,
3549
+ fields: parseEntityTransformParams(req.query),
3550
+ authorizationToken: token
3551
+ });
3552
+ res.status(200).json(response);
3553
+ }).get("/entity-facets", async (req, res) => {
3507
3554
  const response = await entitiesCatalog.facets({
3508
3555
  filter: parseEntityFilterParams(req.query),
3509
3556
  facets: parseEntityFacetParams(req.query),
@@ -3829,6 +3876,27 @@ class AuthorizedEntitiesCatalog {
3829
3876
  }
3830
3877
  return this.entitiesCatalog.entities(request);
3831
3878
  }
3879
+ async entitiesBatch(request) {
3880
+ const authorizeDecision = (await this.permissionApi.authorizeConditional(
3881
+ [{ permission: pluginCatalogCommon.catalogEntityReadPermission }],
3882
+ { token: request == null ? void 0 : request.authorizationToken }
3883
+ ))[0];
3884
+ if (authorizeDecision.result === pluginPermissionCommon.AuthorizeResult.DENY) {
3885
+ return {
3886
+ items: new Array(request.entityRefs.length).fill(null)
3887
+ };
3888
+ }
3889
+ if (authorizeDecision.result === pluginPermissionCommon.AuthorizeResult.CONDITIONAL) {
3890
+ const permissionFilter = this.transformConditions(
3891
+ authorizeDecision.conditions
3892
+ );
3893
+ return this.entitiesCatalog.entitiesBatch({
3894
+ ...request,
3895
+ filter: (request == null ? void 0 : request.filter) ? { allOf: [permissionFilter, request.filter] } : permissionFilter
3896
+ });
3897
+ }
3898
+ return this.entitiesCatalog.entitiesBatch(request);
3899
+ }
3832
3900
  async removeEntityByUid(uid, options) {
3833
3901
  const authorizeResponse = (await this.permissionApi.authorizeConditional(
3834
3902
  [{ permission: pluginCatalogCommon.catalogEntityDeletePermission }],
@@ -4373,7 +4441,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4373
4441
  reader: backendPluginApi.urlReaderServiceRef,
4374
4442
  permissions: backendPluginApi.permissionsServiceRef,
4375
4443
  database: backendPluginApi.databaseServiceRef,
4376
- httpRouter: backendPluginApi.httpRouterServiceRef
4444
+ httpRouter: backendPluginApi.httpRouterServiceRef,
4445
+ lifecycle: backendPluginApi.lifecycleServiceRef
4377
4446
  },
4378
4447
  async init({
4379
4448
  logger,
@@ -4381,7 +4450,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4381
4450
  reader,
4382
4451
  database,
4383
4452
  permissions,
4384
- httpRouter
4453
+ httpRouter,
4454
+ lifecycle
4385
4455
  }) {
4386
4456
  const winstonLogger = backendPluginApi.loggerToWinstonLogger(logger);
4387
4457
  const builder = await CatalogBuilder.create({
@@ -4395,6 +4465,11 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4395
4465
  builder.addEntityProvider(...processingExtensions.entityProviders);
4396
4466
  const { processingEngine, router } = await builder.build();
4397
4467
  await processingEngine.start();
4468
+ lifecycle.addShutdownHook({
4469
+ fn: async () => {
4470
+ await processingEngine.stop();
4471
+ }
4472
+ });
4398
4473
  httpRouter.use(router);
4399
4474
  }
4400
4475
  });