@backstage/plugin-catalog-backend 1.5.1-next.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,60 @@
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
+
33
+ ## 1.5.1
34
+
35
+ ### Patch Changes
36
+
37
+ - c1a4addda3: Improve processing error logging.
38
+
39
+ Adds `location` and `owner` to the logging meta if they are available.
40
+
41
+ - a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`.
42
+ - Updated dependencies
43
+ - @backstage/backend-common@0.16.0
44
+ - @backstage/integration@1.4.0
45
+ - @backstage/catalog-model@1.1.3
46
+ - @backstage/plugin-permission-common@0.7.1
47
+ - @backstage/types@1.0.1
48
+ - @backstage/backend-plugin-api@0.1.4
49
+ - @backstage/plugin-catalog-node@1.2.1
50
+ - @backstage/plugin-permission-node@0.7.1
51
+ - @backstage/catalog-client@1.1.2
52
+ - @backstage/config@1.0.4
53
+ - @backstage/errors@1.1.3
54
+ - @backstage/plugin-catalog-common@1.0.8
55
+ - @backstage/plugin-scaffolder-common@1.2.2
56
+ - @backstage/plugin-search-common@1.1.1
57
+
3
58
  ## 1.5.1-next.1
4
59
 
5
60
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "1.5.1-next.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
@@ -2093,6 +2093,7 @@ class DefaultCatalogProcessingEngine {
2093
2093
  }
2094
2094
  },
2095
2095
  processTask: async (item) => {
2096
+ var _a, _b;
2096
2097
  const track = this.tracker.processStart(item, this.logger);
2097
2098
  try {
2098
2099
  const {
@@ -2130,9 +2131,11 @@ class DefaultCatalogProcessingEngine {
2130
2131
  });
2131
2132
  });
2132
2133
  }
2134
+ const location = (_b = (_a = unprocessedEntity == null ? void 0 : unprocessedEntity.metadata) == null ? void 0 : _a.annotations) == null ? void 0 : _b[catalogModel.ANNOTATION_LOCATION];
2133
2135
  for (const error of result.errors) {
2134
2136
  this.logger.warn(error.message, {
2135
- entity: entityRef
2137
+ entity: entityRef,
2138
+ location
2136
2139
  });
2137
2140
  }
2138
2141
  const errorsString = JSON.stringify(
@@ -2155,8 +2158,8 @@ class DefaultCatalogProcessingEngine {
2155
2158
  if (!result.ok) {
2156
2159
  Promise.resolve(void 0).then(
2157
2160
  () => {
2158
- var _a;
2159
- return (_a = this.onProcessingError) == null ? void 0 : _a.call(this, {
2161
+ var _a2;
2162
+ return (_a2 = this.onProcessingError) == null ? void 0 : _a2.call(this, {
2160
2163
  unprocessedEntity,
2161
2164
  errors: result.errors
2162
2165
  });
@@ -2522,6 +2525,31 @@ class DefaultEntitiesCatalog {
2522
2525
  pageInfo
2523
2526
  };
2524
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
+ }
2525
2553
  async removeEntityByUid(uid) {
2526
2554
  await this.database("refresh_state").update({
2527
2555
  result_hash: "child-was-deleted",
@@ -3227,6 +3255,19 @@ class Stitcher {
3227
3255
  }
3228
3256
  }
3229
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
+
3230
3271
  function basicEntityFilter(items) {
3231
3272
  const filtersByKey = {};
3232
3273
  for (const [key, value] of Object.entries(items)) {
@@ -3366,6 +3407,17 @@ function parseEntityTransformParams(params) {
3366
3407
  };
3367
3408
  }
3368
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
+
3369
3421
  async function requireRequestBody(req) {
3370
3422
  const contentType = req.header("content-type");
3371
3423
  if (!contentType) {
@@ -3402,17 +3454,6 @@ function disallowReadonlyMode(readonly) {
3402
3454
  }
3403
3455
  }
3404
3456
 
3405
- function parseEntityFacetParams(params) {
3406
- const facetStrings = parseStringsParam(params.facet, "facet");
3407
- if (facetStrings) {
3408
- const filtered = facetStrings.filter(Boolean);
3409
- if (filtered.length) {
3410
- return filtered;
3411
- }
3412
- }
3413
- throw new errors.InputError("Missing facet parameter");
3414
- }
3415
-
3416
3457
  async function createRouter(options) {
3417
3458
  const {
3418
3459
  entitiesCatalog,
@@ -3500,7 +3541,16 @@ async function createRouter(options) {
3500
3541
  });
3501
3542
  res.status(200).json(response);
3502
3543
  }
3503
- ).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) => {
3504
3554
  const response = await entitiesCatalog.facets({
3505
3555
  filter: parseEntityFilterParams(req.query),
3506
3556
  facets: parseEntityFacetParams(req.query),
@@ -3826,6 +3876,27 @@ class AuthorizedEntitiesCatalog {
3826
3876
  }
3827
3877
  return this.entitiesCatalog.entities(request);
3828
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
+ }
3829
3900
  async removeEntityByUid(uid, options) {
3830
3901
  const authorizeResponse = (await this.permissionApi.authorizeConditional(
3831
3902
  [{ permission: pluginCatalogCommon.catalogEntityDeletePermission }],
@@ -4370,7 +4441,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4370
4441
  reader: backendPluginApi.urlReaderServiceRef,
4371
4442
  permissions: backendPluginApi.permissionsServiceRef,
4372
4443
  database: backendPluginApi.databaseServiceRef,
4373
- httpRouter: backendPluginApi.httpRouterServiceRef
4444
+ httpRouter: backendPluginApi.httpRouterServiceRef,
4445
+ lifecycle: backendPluginApi.lifecycleServiceRef
4374
4446
  },
4375
4447
  async init({
4376
4448
  logger,
@@ -4378,7 +4450,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4378
4450
  reader,
4379
4451
  database,
4380
4452
  permissions,
4381
- httpRouter
4453
+ httpRouter,
4454
+ lifecycle
4382
4455
  }) {
4383
4456
  const winstonLogger = backendPluginApi.loggerToWinstonLogger(logger);
4384
4457
  const builder = await CatalogBuilder.create({
@@ -4392,6 +4465,11 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4392
4465
  builder.addEntityProvider(...processingExtensions.entityProviders);
4393
4466
  const { processingEngine, router } = await builder.build();
4394
4467
  await processingEngine.start();
4468
+ lifecycle.addShutdownHook({
4469
+ fn: async () => {
4470
+ await processingEngine.stop();
4471
+ }
4472
+ });
4395
4473
  httpRouter.use(router);
4396
4474
  }
4397
4475
  });