@backstage/plugin-catalog-backend 1.9.0-next.3 → 1.9.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,74 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 329b63f4dab: The catalog now has a new, optional `catalog.orphanStrategy` app-config parameter, which can have the string values `'keep'` (default) or `'delete'`.
8
+
9
+ If set to `'keep'` or left unset, the old behavior is maintained of keeping orphaned entities around until manually deleted.
10
+
11
+ If set to `'delete'`, the catalog will attempt to automatically clean out orphaned entities without manual intervention. Note that there are no guarantees that this process is instantaneous, so there may be some delay before orphaned items disappear.
12
+
13
+ For context, the [Life of an Entity](https://backstage.io/docs/features/software-catalog/life-of-an-entity/#orphaning) article goes into some more details on how the nature of orphaning works.
14
+
15
+ To enable the new behavior, you will need to pass the plugin task scheduler to your catalog backend builder. If your code already looks like this, you don't need to change it:
16
+
17
+ ```ts
18
+ // in packages/backend/src/plugins/catalog.ts
19
+ export default async function createPlugin(
20
+ env: PluginEnvironment,
21
+ ): Promise<Router> {
22
+ const builder = await CatalogBuilder.create(env);
23
+ ```
24
+
25
+ But if you pass things into the catalog builder one by one, you'll need to add the new field:
26
+
27
+ ```diff
28
+ // in packages/backend/src/plugins/catalog.ts
29
+ const builder = await CatalogBuilder.create({
30
+ // ... other dependencies
31
+ + scheduler: env.scheduler,
32
+ });
33
+ ```
34
+
35
+ Finally adjust your app-config:
36
+
37
+ ```yaml
38
+ catalog:
39
+ orphanStrategy: delete
40
+ ```
41
+
42
+ - 92a4590fc3a: Add monorepo support to CodeOwnersProccesor.
43
+
44
+ ### Patch Changes
45
+
46
+ - 62a725e3a94: Use the `LocationSpec` type from the `catalog-common` package in place of the deprecated `LocationSpec` from the `catalog-node` package.
47
+ - be5aca50114: Updates and moves OpenAPI spec to `src/schema/openapi.yaml` and uses `ApiRouter` type from `@backstage/backend-openapi-utils` to handle automatic types from the OpenAPI spec file.
48
+ - c9a0fdcd2c8: Fix deprecated types.
49
+ - 899ebfd8e02: Add full text search support to the `by-query` endpoint.
50
+ - 1e4f5e91b8e: Bump `zod` and `zod-to-json-schema` dependencies.
51
+ - c4b846359c0: Allow replacement of the BuiltinKindsEntityProcessor which enables customization of schema validation and connections emitted.
52
+ - c36b89f2af3: Fixed bug in the `DefaultCatalogProcessingEngine` where entities that contained multiple different types of relations for the same source entity would not properly trigger stitching for that source entity.
53
+ - 01ae205352e: Collator factories instantiated in new backend system modules and now marked as deprecated. Will be continued to be exported publicly until the new backend system is fully rolled out.
54
+ - Updated dependencies
55
+ - @backstage/backend-common@0.18.4
56
+ - @backstage/plugin-scaffolder-common@1.2.7
57
+ - @backstage/catalog-client@1.4.1
58
+ - @backstage/plugin-permission-node@0.7.7
59
+ - @backstage/plugin-permission-common@0.7.5
60
+ - @backstage/backend-tasks@0.5.1
61
+ - @backstage/catalog-model@1.3.0
62
+ - @backstage/plugin-search-backend-module-catalog@0.1.0
63
+ - @backstage/integration@1.4.4
64
+ - @backstage/plugin-catalog-node@1.3.5
65
+ - @backstage/backend-plugin-api@0.5.1
66
+ - @backstage/config@1.0.7
67
+ - @backstage/errors@1.1.5
68
+ - @backstage/types@1.0.2
69
+ - @backstage/plugin-catalog-common@1.0.13
70
+ - @backstage/plugin-search-common@1.2.3
71
+
3
72
  ## 1.9.0-next.3
4
73
 
5
74
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "1.9.0-next.3",
3
+ "version": "1.9.0",
4
4
  "main": "../dist/alpha.cjs.js",
5
5
  "types": "../dist/alpha.d.ts"
6
6
  }
package/config.d.ts CHANGED
@@ -81,7 +81,6 @@ export interface Config {
81
81
  * be used in combination with static locations to only serve operator
82
82
  * provided locations. Effectively this removes the ability to register new
83
83
  * components to a running backstage instance.
84
- *
85
84
  */
86
85
  readonly?: boolean;
87
86
 
@@ -136,5 +135,12 @@ export interface Config {
136
135
  allow: Array<string>;
137
136
  }>;
138
137
  }>;
138
+
139
+ /**
140
+ * The strategy to use for entities that are orphaned, i.e. no longer have
141
+ * any other entities or providers referencing them. The default value is
142
+ * "keep".
143
+ */
144
+ orphanStrategy?: 'keep' | 'delete';
139
145
  };
140
146
  }
package/dist/alpha.cjs.js CHANGED
@@ -4,7 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var alpha = require('@backstage/plugin-catalog-common/alpha');
6
6
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
7
- var CatalogBuilder = require('./cjs/CatalogBuilder-c295413f.cjs.js');
7
+ var CatalogBuilder = require('./cjs/CatalogBuilder-ecfd2db6.cjs.js');
8
8
  var backendPluginApi = require('@backstage/backend-plugin-api');
9
9
  var alpha$1 = require('@backstage/plugin-catalog-node/alpha');
10
10
  var backendCommon = require('@backstage/backend-common');
@@ -26,6 +26,7 @@ require('p-limit');
26
26
  require('uuid');
27
27
  require('luxon');
28
28
  require('prom-client');
29
+ require('lodash/uniq');
29
30
  require('fast-json-stable-stringify');
30
31
  require('@opentelemetry/api');
31
32
  require('zod');
@@ -94,7 +95,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
94
95
  permissions: backendPluginApi.coreServices.permissions,
95
96
  database: backendPluginApi.coreServices.database,
96
97
  httpRouter: backendPluginApi.coreServices.httpRouter,
97
- lifecycle: backendPluginApi.coreServices.lifecycle
98
+ lifecycle: backendPluginApi.coreServices.lifecycle,
99
+ scheduler: backendPluginApi.coreServices.scheduler
98
100
  },
99
101
  async init({
100
102
  logger,
@@ -103,7 +105,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
103
105
  database,
104
106
  permissions,
105
107
  httpRouter,
106
- lifecycle
108
+ lifecycle,
109
+ scheduler
107
110
  }) {
108
111
  const winstonLogger = backendCommon.loggerToWinstonLogger(logger);
109
112
  const builder = await CatalogBuilder.CatalogBuilder.create({
@@ -111,6 +114,7 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
111
114
  reader,
112
115
  permissions,
113
116
  database,
117
+ scheduler,
114
118
  logger: winstonLogger
115
119
  });
116
120
  builder.addProcessor(...processingExtensions.processors);
@@ -1 +1 @@
1
- {"version":3,"file":"alpha.cjs.js","sources":["../src/permissions/conditionExports.ts","../src/service/CatalogPlugin.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha';\nimport { createConditionExports } from '@backstage/plugin-permission-node';\nimport { permissionRules } from './rules';\n\nconst { conditions, createConditionalDecision } = createConditionExports({\n pluginId: 'catalog',\n resourceType: RESOURCE_TYPE_CATALOG_ENTITY,\n rules: permissionRules,\n});\n\n/**\n * These conditions are used when creating conditional decisions for catalog\n * entities that are returned by authorization policies.\n *\n * @alpha\n */\nexport const catalogConditions = conditions;\n\n/**\n * `createCatalogConditionalDecision` can be used when authoring policies to\n * create conditional decisions. It requires a permission of type\n * `ResourcePermission<'catalog-entity'>` to be passed as the first parameter.\n * It's recommended that you use the provided `isResourcePermission` and\n * `isPermission` helper methods to narrow the type of the permission passed to\n * the handle method as shown below.\n *\n * ```\n * // MyAuthorizationPolicy.ts\n * ...\n * import { createCatalogPolicyDecision } from '@backstage/plugin-catalog-backend';\n * import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common';\n *\n * class MyAuthorizationPolicy implements PermissionPolicy {\n * async handle(request, user) {\n * ...\n *\n * if (isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY)) {\n * return createCatalogConditionalDecision(\n * request.permission,\n * { anyOf: [...insert conditions here...] }\n * );\n * }\n *\n * ...\n * }\n * ```\n *\n * @alpha\n */\nexport const createCatalogConditionalDecision = createConditionalDecision;\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n createBackendPlugin,\n coreServices,\n} from '@backstage/backend-plugin-api';\nimport { CatalogBuilder } from './CatalogBuilder';\nimport {\n CatalogProcessingExtensionPoint,\n catalogProcessingExtensionPoint,\n} from '@backstage/plugin-catalog-node/alpha';\nimport {\n CatalogProcessor,\n EntityProvider,\n} from '@backstage/plugin-catalog-node';\nimport { loggerToWinstonLogger } from '@backstage/backend-common';\n\nclass CatalogExtensionPointImpl implements CatalogProcessingExtensionPoint {\n #processors = new Array<CatalogProcessor>();\n #entityProviders = new Array<EntityProvider>();\n\n addProcessor(\n ...processors: Array<CatalogProcessor | Array<CatalogProcessor>>\n ): void {\n this.#processors.push(...processors.flat());\n }\n\n addEntityProvider(\n ...providers: Array<EntityProvider | Array<EntityProvider>>\n ): void {\n this.#entityProviders.push(...providers.flat());\n }\n\n get processors() {\n return this.#processors;\n }\n\n get entityProviders() {\n return this.#entityProviders;\n }\n}\n\n/**\n * Catalog plugin\n * @alpha\n */\nexport const catalogPlugin = createBackendPlugin({\n pluginId: 'catalog',\n register(env) {\n const processingExtensions = new CatalogExtensionPointImpl();\n // plugins depending on this API will be initialized before this plugins init method is executed.\n env.registerExtensionPoint(\n catalogProcessingExtensionPoint,\n processingExtensions,\n );\n\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.config,\n reader: coreServices.urlReader,\n permissions: coreServices.permissions,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n lifecycle: coreServices.lifecycle,\n },\n async init({\n logger,\n config,\n reader,\n database,\n permissions,\n httpRouter,\n lifecycle,\n }) {\n const winstonLogger = loggerToWinstonLogger(logger);\n const builder = await CatalogBuilder.create({\n config,\n reader,\n permissions,\n database,\n logger: winstonLogger,\n });\n builder.addProcessor(...processingExtensions.processors);\n builder.addEntityProvider(...processingExtensions.entityProviders);\n const { processingEngine, router } = await builder.build();\n\n await processingEngine.start();\n lifecycle.addShutdownHook(() => processingEngine.stop());\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createConditionExports","RESOURCE_TYPE_CATALOG_ENTITY","permissionRules","createBackendPlugin","catalogProcessingExtensionPoint","coreServices","loggerToWinstonLogger","CatalogBuilder"],"mappings":"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"}
1
+ {"version":3,"file":"alpha.cjs.js","sources":["../src/permissions/conditionExports.ts","../src/service/CatalogPlugin.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha';\nimport { createConditionExports } from '@backstage/plugin-permission-node';\nimport { permissionRules } from './rules';\n\nconst { conditions, createConditionalDecision } = createConditionExports({\n pluginId: 'catalog',\n resourceType: RESOURCE_TYPE_CATALOG_ENTITY,\n rules: permissionRules,\n});\n\n/**\n * These conditions are used when creating conditional decisions for catalog\n * entities that are returned by authorization policies.\n *\n * @alpha\n */\nexport const catalogConditions = conditions;\n\n/**\n * `createCatalogConditionalDecision` can be used when authoring policies to\n * create conditional decisions. It requires a permission of type\n * `ResourcePermission<'catalog-entity'>` to be passed as the first parameter.\n * It's recommended that you use the provided `isResourcePermission` and\n * `isPermission` helper methods to narrow the type of the permission passed to\n * the handle method as shown below.\n *\n * ```\n * // MyAuthorizationPolicy.ts\n * ...\n * import { createCatalogPolicyDecision } from '@backstage/plugin-catalog-backend';\n * import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common';\n *\n * class MyAuthorizationPolicy implements PermissionPolicy {\n * async handle(request, user) {\n * ...\n *\n * if (isResourcePermission(request.permission, RESOURCE_TYPE_CATALOG_ENTITY)) {\n * return createCatalogConditionalDecision(\n * request.permission,\n * { anyOf: [...insert conditions here...] }\n * );\n * }\n *\n * ...\n * }\n * ```\n *\n * @alpha\n */\nexport const createCatalogConditionalDecision = createConditionalDecision;\n","/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n createBackendPlugin,\n coreServices,\n} from '@backstage/backend-plugin-api';\nimport { CatalogBuilder } from './CatalogBuilder';\nimport {\n CatalogProcessingExtensionPoint,\n catalogProcessingExtensionPoint,\n} from '@backstage/plugin-catalog-node/alpha';\nimport {\n CatalogProcessor,\n EntityProvider,\n} from '@backstage/plugin-catalog-node';\nimport { loggerToWinstonLogger } from '@backstage/backend-common';\n\nclass CatalogExtensionPointImpl implements CatalogProcessingExtensionPoint {\n #processors = new Array<CatalogProcessor>();\n #entityProviders = new Array<EntityProvider>();\n\n addProcessor(\n ...processors: Array<CatalogProcessor | Array<CatalogProcessor>>\n ): void {\n this.#processors.push(...processors.flat());\n }\n\n addEntityProvider(\n ...providers: Array<EntityProvider | Array<EntityProvider>>\n ): void {\n this.#entityProviders.push(...providers.flat());\n }\n\n get processors() {\n return this.#processors;\n }\n\n get entityProviders() {\n return this.#entityProviders;\n }\n}\n\n/**\n * Catalog plugin\n * @alpha\n */\nexport const catalogPlugin = createBackendPlugin({\n pluginId: 'catalog',\n register(env) {\n const processingExtensions = new CatalogExtensionPointImpl();\n // plugins depending on this API will be initialized before this plugins init method is executed.\n env.registerExtensionPoint(\n catalogProcessingExtensionPoint,\n processingExtensions,\n );\n\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.config,\n reader: coreServices.urlReader,\n permissions: coreServices.permissions,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n lifecycle: coreServices.lifecycle,\n scheduler: coreServices.scheduler,\n },\n async init({\n logger,\n config,\n reader,\n database,\n permissions,\n httpRouter,\n lifecycle,\n scheduler,\n }) {\n const winstonLogger = loggerToWinstonLogger(logger);\n const builder = await CatalogBuilder.create({\n config,\n reader,\n permissions,\n database,\n scheduler,\n logger: winstonLogger,\n });\n builder.addProcessor(...processingExtensions.processors);\n builder.addEntityProvider(...processingExtensions.entityProviders);\n const { processingEngine, router } = await builder.build();\n\n await processingEngine.start();\n lifecycle.addShutdownHook(() => processingEngine.stop());\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createConditionExports","RESOURCE_TYPE_CATALOG_ENTITY","permissionRules","createBackendPlugin","catalogProcessingExtensionPoint","coreServices","loggerToWinstonLogger","CatalogBuilder"],"mappings":"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"}
@@ -19,6 +19,7 @@ var uuid = require('uuid');
19
19
  var backendCommon = require('@backstage/backend-common');
20
20
  var luxon = require('luxon');
21
21
  var promClient = require('prom-client');
22
+ var uniq = require('lodash/uniq');
22
23
  var stableStringify = require('fast-json-stable-stringify');
23
24
  var api = require('@opentelemetry/api');
24
25
  var zod = require('zod');
@@ -59,6 +60,7 @@ var g__default = /*#__PURE__*/_interopDefaultLegacy(g);
59
60
  var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
60
61
  var yaml__default = /*#__PURE__*/_interopDefaultLegacy(yaml);
61
62
  var limiterFactory__default = /*#__PURE__*/_interopDefaultLegacy(limiterFactory);
63
+ var uniq__default = /*#__PURE__*/_interopDefaultLegacy(uniq);
62
64
  var stableStringify__default = /*#__PURE__*/_interopDefaultLegacy(stableStringify);
63
65
  var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
64
66
  var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
@@ -1169,6 +1171,48 @@ async function updateUnprocessedEntity(options) {
1169
1171
  return refreshResult === 1;
1170
1172
  }
1171
1173
 
1174
+ async function deleteOrphanedEntities(options) {
1175
+ const { tx } = options;
1176
+ let total = 0;
1177
+ for (let i = 0; i < 100; ++i) {
1178
+ const candidates = await tx.with(
1179
+ "orphans",
1180
+ (orphans) => orphans.from("refresh_state").select("entity_id", "entity_ref").whereNotIn(
1181
+ "entity_ref",
1182
+ (keep) => keep.distinct("target_entity_ref").from("refresh_state_references")
1183
+ )
1184
+ ).select({
1185
+ entityId: "orphans.entity_id",
1186
+ relationSourceId: "refresh_state.entity_id"
1187
+ }).from("orphans").leftOuterJoin(
1188
+ "relations",
1189
+ "relations.target_entity_ref",
1190
+ "orphans.entity_ref"
1191
+ ).leftOuterJoin(
1192
+ "refresh_state",
1193
+ "refresh_state.entity_ref",
1194
+ "relations.source_entity_ref"
1195
+ );
1196
+ if (!candidates.length) {
1197
+ break;
1198
+ }
1199
+ const orphanIds = uniq__default["default"](candidates.map((r) => r.entityId));
1200
+ const orphanRelationIds = uniq__default["default"](
1201
+ candidates.map((r) => r.relationSourceId).filter(Boolean)
1202
+ );
1203
+ total += orphanIds.length;
1204
+ await tx.table("refresh_state").delete().whereIn("entity_id", orphanIds);
1205
+ await tx.table("final_entities").update({
1206
+ hash: "orphan-relation-deleted"
1207
+ }).whereIn("entity_id", orphanRelationIds);
1208
+ await tx.table("refresh_state").update({
1209
+ result_hash: "orphan-relation-deleted",
1210
+ next_update_at: tx.fn.now()
1211
+ }).whereIn("entity_id", orphanRelationIds);
1212
+ }
1213
+ return total;
1214
+ }
1215
+
1172
1216
  function generateStableHash$1(entity) {
1173
1217
  return crypto.createHash("sha1").update(stableStringify__default["default"]({ ...entity })).digest("hex");
1174
1218
  }
@@ -1309,6 +1353,10 @@ class DefaultProcessingDatabase {
1309
1353
  const entityRefs = rows.map((r) => r.source_entity_ref).filter(Boolean);
1310
1354
  return { entityRefs };
1311
1355
  }
1356
+ async deleteOrphanedEntities(txOpaque) {
1357
+ const tx = txOpaque;
1358
+ return await deleteOrphanedEntities({ tx });
1359
+ }
1312
1360
  async transaction(fn) {
1313
1361
  try {
1314
1362
  let result = void 0;
@@ -1445,21 +1493,40 @@ function startTaskPipeline(options) {
1445
1493
 
1446
1494
  const CACHE_TTL = 5;
1447
1495
  class DefaultCatalogProcessingEngine {
1448
- constructor(logger, processingDatabase, orchestrator, stitcher, createHash, pollingIntervalMs = 1e3, onProcessingError, tracker = progressTracker()) {
1449
- this.logger = logger;
1450
- this.processingDatabase = processingDatabase;
1451
- this.orchestrator = orchestrator;
1452
- this.stitcher = stitcher;
1453
- this.createHash = createHash;
1454
- this.pollingIntervalMs = pollingIntervalMs;
1455
- this.onProcessingError = onProcessingError;
1456
- this.tracker = tracker;
1496
+ constructor(options) {
1497
+ var _a, _b, _c;
1498
+ this.config = options.config;
1499
+ this.scheduler = options.scheduler;
1500
+ this.logger = options.logger;
1501
+ this.processingDatabase = options.processingDatabase;
1502
+ this.orchestrator = options.orchestrator;
1503
+ this.stitcher = options.stitcher;
1504
+ this.createHash = options.createHash;
1505
+ this.pollingIntervalMs = (_a = options.pollingIntervalMs) != null ? _a : 1e3;
1506
+ this.orphanCleanupIntervalMs = (_b = options.orphanCleanupIntervalMs) != null ? _b : 3e4;
1507
+ this.onProcessingError = options.onProcessingError;
1508
+ this.tracker = (_c = options.tracker) != null ? _c : progressTracker();
1509
+ this.stopFunc = void 0;
1457
1510
  }
1458
1511
  async start() {
1459
1512
  if (this.stopFunc) {
1460
1513
  throw new Error("Processing engine is already started");
1461
1514
  }
1462
- this.stopFunc = startTaskPipeline({
1515
+ const stopPipeline = this.startPipeline();
1516
+ const stopCleanup = this.startOrphanCleanup();
1517
+ this.stopFunc = () => {
1518
+ stopPipeline();
1519
+ stopCleanup();
1520
+ };
1521
+ }
1522
+ async stop() {
1523
+ if (this.stopFunc) {
1524
+ this.stopFunc();
1525
+ this.stopFunc = void 0;
1526
+ }
1527
+ }
1528
+ startPipeline() {
1529
+ return startTaskPipeline({
1463
1530
  lowWatermark: 5,
1464
1531
  highWatermark: 10,
1465
1532
  pollingIntervalMs: this.pollingIntervalMs,
@@ -1618,11 +1685,40 @@ class DefaultCatalogProcessingEngine {
1618
1685
  }
1619
1686
  });
1620
1687
  }
1621
- async stop() {
1622
- if (this.stopFunc) {
1623
- this.stopFunc();
1624
- this.stopFunc = void 0;
1688
+ startOrphanCleanup() {
1689
+ var _a;
1690
+ const strategy = (_a = this.config.getOptionalString("catalog.orphanStrategy")) != null ? _a : "keep";
1691
+ if (strategy !== "delete") {
1692
+ return () => {
1693
+ };
1694
+ }
1695
+ const runOnce = async () => {
1696
+ try {
1697
+ await this.processingDatabase.transaction(async (tx) => {
1698
+ const n = await this.processingDatabase.deleteOrphanedEntities(tx);
1699
+ this.logger.info(`Deleted ${n} orphaned entities`);
1700
+ });
1701
+ } catch (error) {
1702
+ this.logger.warn(`Failed to delete orphaned entities`, error);
1703
+ }
1704
+ };
1705
+ if (this.scheduler) {
1706
+ const abortController = new AbortController();
1707
+ this.scheduler.scheduleTask({
1708
+ id: "catalog_orphan_cleanup",
1709
+ frequency: { milliseconds: this.orphanCleanupIntervalMs },
1710
+ timeout: { milliseconds: this.orphanCleanupIntervalMs * 0.8 },
1711
+ fn: runOnce,
1712
+ signal: abortController.signal
1713
+ });
1714
+ return () => {
1715
+ abortController.abort();
1716
+ };
1625
1717
  }
1718
+ const intervalKey = setInterval(runOnce, this.orphanCleanupIntervalMs);
1719
+ return () => {
1720
+ clearInterval(intervalKey);
1721
+ };
1626
1722
  }
1627
1723
  }
1628
1724
  function progressTracker() {
@@ -1652,9 +1748,7 @@ function progressTracker() {
1652
1748
  const meter = api.metrics.getMeter("default");
1653
1749
  const stitchedEntities = meter.createCounter(
1654
1750
  "catalog.stitched.entities.count",
1655
- {
1656
- description: "Amount of entities stitched"
1657
- }
1751
+ { description: "Amount of entities stitched" }
1658
1752
  );
1659
1753
  const processedEntities = meter.createCounter(
1660
1754
  "catalog.processed.entities.count",
@@ -4684,7 +4778,7 @@ class CatalogBuilder {
4684
4778
  */
4685
4779
  async build() {
4686
4780
  var _a, _b;
4687
- const { config, database, logger, permissions } = this.env;
4781
+ const { config, database, logger, permissions, scheduler } = this.env;
4688
4782
  const policy = this.buildEntityPolicy();
4689
4783
  const processors = this.buildProcessors();
4690
4784
  const parser = this.parser || defaultEntityDataParser;
@@ -4766,18 +4860,20 @@ class CatalogBuilder {
4766
4860
  [...this.entityProviders, locationStore, configLocationProvider],
4767
4861
  (provider) => provider.getProviderName()
4768
4862
  );
4769
- const processingEngine = new DefaultCatalogProcessingEngine(
4863
+ const processingEngine = new DefaultCatalogProcessingEngine({
4864
+ config,
4865
+ scheduler,
4770
4866
  logger,
4771
4867
  processingDatabase,
4772
4868
  orchestrator,
4773
4869
  stitcher,
4774
- () => crypto.createHash("sha1"),
4775
- 1e3,
4776
- (event) => {
4870
+ createHash: () => crypto.createHash("sha1"),
4871
+ pollingIntervalMs: 1e3,
4872
+ onProcessingError: (event) => {
4777
4873
  var _a2;
4778
4874
  (_a2 = this.onProcessingError) == null ? void 0 : _a2.call(this, event);
4779
4875
  }
4780
- );
4876
+ });
4781
4877
  const locationAnalyzer = (_b = this.locationAnalyzer) != null ? _b : new RepoLocationAnalyzer(logger, integrations, this.locationAnalyzers);
4782
4878
  const locationService = new AuthorizedLocationService(
4783
4879
  new DefaultLocationService(locationStore, orchestrator, {
@@ -4960,4 +5056,4 @@ exports.createCatalogPermissionRule = createCatalogPermissionRule;
4960
5056
  exports.createRandomProcessingInterval = createRandomProcessingInterval;
4961
5057
  exports.parseEntityYaml = parseEntityYaml;
4962
5058
  exports.permissionRules = permissionRules;
4963
- //# sourceMappingURL=CatalogBuilder-c295413f.cjs.js.map
5059
+ //# sourceMappingURL=CatalogBuilder-ecfd2db6.cjs.js.map