@backstage/plugin-catalog-backend 0.17.4 → 0.18.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,72 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7f82ce9f51: **BREAKING** EntitiesSearchFilter fields have changed.
8
+
9
+ EntitiesSearchFilter now has only two fields: `key` and `value`. The `matchValueIn` and `matchValueExists` fields are no longer are supported. Previous filters written using the `matchValueIn` and `matchValueExists` fields can be rewritten as follows:
10
+
11
+ Filtering by existence of key only:
12
+
13
+ ```diff
14
+ filter: {
15
+ {
16
+ key: 'abc',
17
+ - matchValueExists: true,
18
+ },
19
+ }
20
+ ```
21
+
22
+ Filtering by key and values:
23
+
24
+ ```diff
25
+ filter: {
26
+ {
27
+ key: 'abc',
28
+ - matchValueExists: true,
29
+ - matchValueIn: ['xyz'],
30
+ + values: ['xyz'],
31
+ },
32
+ }
33
+ ```
34
+
35
+ Negation of filters can now be achieved through a `not` object:
36
+
37
+ ```
38
+ filter: {
39
+ not: {
40
+ key: 'abc',
41
+ values: ['xyz'],
42
+ },
43
+ }
44
+ ```
45
+
46
+ ### Patch Changes
47
+
48
+ - 740f958290: Providing an empty values array in an EntityFilter will now return no matches.
49
+ - bab752e2b3: Change default port of backend from 7000 to 7007.
50
+
51
+ This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start.
52
+
53
+ You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values:
54
+
55
+ ```
56
+ backend:
57
+ listen: 0.0.0.0:7123
58
+ baseUrl: http://localhost:7123
59
+ ```
60
+
61
+ More information can be found here: https://backstage.io/docs/conf/writing
62
+
63
+ - eddb82ab7c: Index User entities by displayName to be able to search by full name. Added displayName (if present) to the 'text' field in the indexed document.
64
+ - 563b039f0b: Added Azure DevOps discovery processor
65
+ - 8866b62f3d: Detect a duplicate entities when adding locations through dry run
66
+ - Updated dependencies
67
+ - @backstage/errors@0.1.5
68
+ - @backstage/backend-common@0.9.11
69
+
3
70
  ## 0.17.4
4
71
 
5
72
  ### Patch Changes
package/dist/index.cjs.js CHANGED
@@ -542,7 +542,7 @@ class BitbucketDiscoveryProcessor {
542
542
  }
543
543
  async processOrganisationRepositories(options) {
544
544
  const {client, location, integration, emit} = options;
545
- const {catalogPath: requestedCatalogPath} = parseUrl$2(location.target);
545
+ const {catalogPath: requestedCatalogPath} = parseUrl$3(location.target);
546
546
  const catalogPath = requestedCatalogPath === EMPTY_CATALOG_LOCATION ? DEFAULT_CATALOG_LOCATION : requestedCatalogPath;
547
547
  const result = await readBitbucketOrg(client, location.target);
548
548
  for (const repository of result.matches) {
@@ -561,7 +561,7 @@ class BitbucketDiscoveryProcessor {
561
561
  }
562
562
  }
563
563
  async function readBitbucketOrg(client, target) {
564
- const {projectSearchPath, repoSearchPath} = parseUrl$2(target);
564
+ const {projectSearchPath, repoSearchPath} = parseUrl$3(target);
565
565
  const projects = paginated$1((options) => client.listProjects(options));
566
566
  const result = {
567
567
  scanned: 0,
@@ -603,7 +603,7 @@ async function readBitbucketCloud(client, target) {
603
603
  }
604
604
  return result;
605
605
  }
606
- function parseUrl$2(urlString) {
606
+ function parseUrl$3(urlString) {
607
607
  const url = new URL(urlString);
608
608
  const path = url.pathname.substr(1).split("/");
609
609
  if (path.length > 3 && path[1].length && path[3].length) {
@@ -1115,7 +1115,7 @@ class GithubDiscoveryProcessor {
1115
1115
  if (!gitHubConfig) {
1116
1116
  throw new Error(`There is no GitHub integration that matches ${location$1.target}. Please add a configuration entry for it under integrations.github`);
1117
1117
  }
1118
- const {org, repoSearchPath, catalogPath, branch, host} = parseUrl$1(location$1.target);
1118
+ const {org, repoSearchPath, catalogPath, branch, host} = parseUrl$2(location$1.target);
1119
1119
  const orgUrl = `https://${host}/${org}`;
1120
1120
  const {headers} = await integration.GithubCredentialsProvider.create(gitHubConfig).getCredentials({url: orgUrl});
1121
1121
  const client = graphql.graphql.defaults({
@@ -1143,7 +1143,7 @@ class GithubDiscoveryProcessor {
1143
1143
  return true;
1144
1144
  }
1145
1145
  }
1146
- function parseUrl$1(urlString) {
1146
+ function parseUrl$2(urlString) {
1147
1147
  const url = new URL(urlString);
1148
1148
  const path = url.pathname.substr(1).split("/");
1149
1149
  if (path.length > 2 && path[0].length && path[1].length) {
@@ -1169,6 +1169,93 @@ function escapeRegExp(str) {
1169
1169
  return new RegExp(`^${str.replace(/\*/g, ".*")}$`);
1170
1170
  }
1171
1171
 
1172
+ const isCloud = (host) => host === "dev.azure.com";
1173
+ const PAGE_SIZE = 1e3;
1174
+ async function codeSearch(azureConfig, org, project, repo, path) {
1175
+ const searchBaseUrl = isCloud(azureConfig.host) ? "https://almsearch.dev.azure.com" : `https://${azureConfig.host}`;
1176
+ const searchUrl = `${searchBaseUrl}/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`;
1177
+ let items = [];
1178
+ let hasMorePages = true;
1179
+ do {
1180
+ const response = await fetch__default['default'](searchUrl, {
1181
+ ...integration.getAzureRequestOptions(azureConfig, {
1182
+ "Content-Type": "application/json"
1183
+ }),
1184
+ method: "POST",
1185
+ body: JSON.stringify({
1186
+ searchText: `path:${path} repo:${repo || "*"}`,
1187
+ $skip: items.length,
1188
+ $top: PAGE_SIZE
1189
+ })
1190
+ });
1191
+ if (response.status !== 200) {
1192
+ throw new Error(`Azure DevOps search failed with response status ${response.status}`);
1193
+ }
1194
+ const body = await response.json();
1195
+ items = [...items, ...body.results];
1196
+ hasMorePages = body.count > items.length;
1197
+ } while (hasMorePages);
1198
+ return items;
1199
+ }
1200
+
1201
+ class AzureDevOpsDiscoveryProcessor {
1202
+ static fromConfig(config, options) {
1203
+ const integrations = integration.ScmIntegrations.fromConfig(config);
1204
+ return new AzureDevOpsDiscoveryProcessor({
1205
+ ...options,
1206
+ integrations
1207
+ });
1208
+ }
1209
+ constructor(options) {
1210
+ this.integrations = options.integrations;
1211
+ this.logger = options.logger;
1212
+ }
1213
+ async readLocation(location$1, _optional, emit) {
1214
+ var _a;
1215
+ if (location$1.type !== "azure-discovery") {
1216
+ return false;
1217
+ }
1218
+ const azureConfig = (_a = this.integrations.azure.byUrl(location$1.target)) == null ? void 0 : _a.config;
1219
+ if (!azureConfig) {
1220
+ throw new Error(`There is no Azure integration that matches ${location$1.target}. Please add a configuration entry for it under integrations.azure`);
1221
+ }
1222
+ const {baseUrl, org, project, repo, catalogPath} = parseUrl$1(location$1.target);
1223
+ this.logger.info(`Reading Azure DevOps repositories from ${location$1.target}`);
1224
+ const files = await codeSearch(azureConfig, org, project, repo, catalogPath);
1225
+ this.logger.debug(`Found ${files.length} files in Azure DevOps from ${location$1.target}.`);
1226
+ for (const file of files) {
1227
+ emit(location({
1228
+ type: "url",
1229
+ target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`
1230
+ }, true));
1231
+ }
1232
+ return true;
1233
+ }
1234
+ }
1235
+ function parseUrl$1(urlString) {
1236
+ const url = new URL(urlString);
1237
+ const path = url.pathname.substr(1).split("/");
1238
+ const catalogPath = url.searchParams.get("path") || "/catalog-info.yaml";
1239
+ if (path.length === 2 && path[0].length && path[1].length) {
1240
+ return {
1241
+ baseUrl: url.origin,
1242
+ org: decodeURIComponent(path[0]),
1243
+ project: decodeURIComponent(path[1]),
1244
+ repo: "",
1245
+ catalogPath
1246
+ };
1247
+ } else if (path.length === 4 && path[0].length && path[1].length && path[2].length && path[3].length) {
1248
+ return {
1249
+ baseUrl: url.origin,
1250
+ org: decodeURIComponent(path[0]),
1251
+ project: decodeURIComponent(path[1]),
1252
+ repo: decodeURIComponent(path[3]),
1253
+ catalogPath
1254
+ };
1255
+ }
1256
+ throw new Error(`Failed to parse ${urlString}`);
1257
+ }
1258
+
1172
1259
  function buildOrgHierarchy(groups) {
1173
1260
  const groupsByName = new Map(groups.map((g) => [g.metadata.name, g]));
1174
1261
  for (const group of groups) {
@@ -1829,8 +1916,8 @@ function basicEntityFilter(items) {
1829
1916
  const filtersByKey = {};
1830
1917
  for (const [key, value] of Object.entries(items)) {
1831
1918
  const values = [value].flat();
1832
- const f = key in filtersByKey ? filtersByKey[key] : filtersByKey[key] = {key, matchValueIn: []};
1833
- f.matchValueIn.push(...values);
1919
+ const f = key in filtersByKey ? filtersByKey[key] : filtersByKey[key] = {key, values: []};
1920
+ f.values.push(...values);
1834
1921
  }
1835
1922
  return {anyOf: [{allOf: Object.values(filtersByKey)}]};
1836
1923
  }
@@ -1893,11 +1980,9 @@ function parseEntityFilterString(filterString) {
1893
1980
  throw new errors.InputError(`Invalid filter, '${statement}' is not a valid statement (expected a string on the form a=b or a= or a)`);
1894
1981
  }
1895
1982
  const f = key in filtersByKey ? filtersByKey[key] : filtersByKey[key] = {key};
1896
- if (value === void 0) {
1897
- f.matchValueExists = true;
1898
- } else {
1899
- f.matchValueIn = f.matchValueIn || [];
1900
- f.matchValueIn.push(value);
1983
+ if (value !== void 0) {
1984
+ f.values = f.values || [];
1985
+ f.values.push(value);
1901
1986
  }
1902
1987
  }
1903
1988
  return Object.values(filtersByKey);
@@ -2633,27 +2718,27 @@ class CommonDatabase {
2633
2718
  var _a, _b;
2634
2719
  const tx = txOpaque;
2635
2720
  let entitiesQuery = tx("entities");
2636
- if ((request == null ? void 0 : request.filter) && (request.filter.hasOwnProperty("key") || request.filter.hasOwnProperty("allOf"))) {
2721
+ if ((request == null ? void 0 : request.filter) && (request.filter.hasOwnProperty("key") || request.filter.hasOwnProperty("allOf") || request.filter.hasOwnProperty("not"))) {
2637
2722
  throw new Error("Filters for the legacy CommonDatabase must obey the { anyOf: [{ allOf: [] }] } format.");
2638
2723
  }
2639
2724
  for (const singleFilter of (_b = (_a = request == null ? void 0 : request.filter) == null ? void 0 : _a.anyOf) != null ? _b : []) {
2640
2725
  entitiesQuery = entitiesQuery.orWhere(function singleFilterFn() {
2641
2726
  for (const filter of singleFilter.allOf) {
2642
- if (filter.hasOwnProperty("anyOf") || filter.hasOwnProperty("allOf")) {
2727
+ if (filter.hasOwnProperty("anyOf") || filter.hasOwnProperty("allOf") || filter.hasOwnProperty("not")) {
2643
2728
  throw new Error("Nested filters are not supported in the legacy CommonDatabase");
2644
2729
  }
2645
- const {key, matchValueIn, matchValueExists} = filter;
2730
+ const {key, values} = filter;
2646
2731
  const matchQuery = tx("entities_search").select("entity_id").where(function keyFilter() {
2647
2732
  this.andWhere({key: key.toLowerCase()});
2648
- if (matchValueExists !== false && matchValueIn) {
2649
- if (matchValueIn.length === 1) {
2650
- this.andWhere({value: matchValueIn[0].toLowerCase()});
2651
- } else if (matchValueIn.length > 1) {
2652
- this.andWhere("value", "in", matchValueIn.map((v) => v.toLowerCase()));
2733
+ if (values) {
2734
+ if (values.length === 1) {
2735
+ this.andWhere({value: values[0].toLowerCase()});
2736
+ } else if (values.length > 1) {
2737
+ this.andWhere("value", "in", values.map((v) => v.toLowerCase()));
2653
2738
  }
2654
2739
  }
2655
2740
  });
2656
- this.andWhere("id", matchValueExists === false ? "not in" : "in", matchQuery);
2741
+ this.andWhere("id", "in", matchQuery);
2657
2742
  }
2658
2743
  });
2659
2744
  }
@@ -3919,6 +4004,29 @@ class DefaultLocationService {
3919
4004
  deleteLocation(id) {
3920
4005
  return this.store.deleteLocation(id);
3921
4006
  }
4007
+ async processEntities(unprocessedEntities) {
4008
+ const entities = [];
4009
+ while (unprocessedEntities.length) {
4010
+ const currentEntity = unprocessedEntities.pop();
4011
+ if (!currentEntity) {
4012
+ continue;
4013
+ }
4014
+ const processed = await this.orchestrator.process({
4015
+ entity: currentEntity.entity,
4016
+ state: {}
4017
+ });
4018
+ if (processed.ok) {
4019
+ if (entities.some((e) => catalogModel.stringifyEntityRef(e) === catalogModel.stringifyEntityRef(processed.completedEntity))) {
4020
+ throw new Error(`Duplicate nested entity: ${catalogModel.stringifyEntityRef(processed.completedEntity)}`);
4021
+ }
4022
+ unprocessedEntities.push(...processed.deferredEntities);
4023
+ entities.push(processed.completedEntity);
4024
+ } else {
4025
+ throw Error(processed.errors.map(String).join(", "));
4026
+ }
4027
+ }
4028
+ return entities;
4029
+ }
3922
4030
  async dryRunCreateLocation(spec) {
3923
4031
  const existsPromise = this.store.listLocations().then((locations) => locations.some((l) => l.type === spec.type && l.target === spec.target));
3924
4032
  const entity = {
@@ -3943,23 +4051,7 @@ class DefaultLocationService {
3943
4051
  const unprocessedEntities = [
3944
4052
  {entity, locationKey: `${spec.type}:${spec.target}`}
3945
4053
  ];
3946
- const entities = [];
3947
- while (unprocessedEntities.length) {
3948
- const currentEntity = unprocessedEntities.pop();
3949
- if (!currentEntity) {
3950
- continue;
3951
- }
3952
- const processed = await this.orchestrator.process({
3953
- entity: currentEntity.entity,
3954
- state: {}
3955
- });
3956
- if (processed.ok) {
3957
- unprocessedEntities.push(...processed.deferredEntities);
3958
- entities.push(processed.completedEntity);
3959
- } else {
3960
- throw Error(processed.errors.map(String).join(", "));
3961
- }
3962
- }
4054
+ const entities = await this.processEntities(unprocessedEntities);
3963
4055
  return {
3964
4056
  exists: await existsPromise,
3965
4057
  location: {...spec, id: `${spec.type}:${spec.target}`},
@@ -4088,51 +4180,48 @@ function stringifyPagination(input) {
4088
4180
  const base64 = Buffer.from(json, "utf8").toString("base64");
4089
4181
  return base64;
4090
4182
  }
4091
- function addCondition(queryBuilder, db, {key, matchValueIn, matchValueExists}) {
4092
- const matchQuery = db("search").select("entity_id").where(function keyFilter() {
4093
- this.andWhere({key: key.toLowerCase()});
4094
- if (matchValueExists !== false && matchValueIn) {
4095
- if (matchValueIn.length === 1) {
4096
- this.andWhere({value: matchValueIn[0].toLowerCase()});
4097
- } else if (matchValueIn.length > 1) {
4098
- this.andWhere("value", "in", matchValueIn.map((v) => v.toLowerCase()));
4183
+ function addCondition(queryBuilder, db, filter, negate = false) {
4184
+ const matchQuery = db("search").select("entity_id").where({key: filter.key.toLowerCase()}).andWhere(function keyFilter() {
4185
+ if (filter.values) {
4186
+ if (filter.values.length === 1) {
4187
+ this.where({value: filter.values[0].toLowerCase()});
4188
+ } else {
4189
+ this.andWhere("value", "in", filter.values.map((v) => v.toLowerCase()));
4099
4190
  }
4100
4191
  }
4101
4192
  });
4102
- queryBuilder.andWhere("entity_id", matchValueExists === false ? "not in" : "in", matchQuery);
4193
+ queryBuilder.andWhere("entity_id", negate ? "not in" : "in", matchQuery);
4103
4194
  }
4104
4195
  function isEntitiesSearchFilter(filter) {
4105
4196
  return filter.hasOwnProperty("key");
4106
4197
  }
4107
- function isAndEntityFilter(filter) {
4108
- return filter.hasOwnProperty("allOf");
4109
- }
4110
4198
  function isOrEntityFilter(filter) {
4111
4199
  return filter.hasOwnProperty("anyOf");
4112
4200
  }
4113
- function parseFilter(filter, query, db) {
4201
+ function isNegationEntityFilter(filter) {
4202
+ return filter.hasOwnProperty("not");
4203
+ }
4204
+ function parseFilter(filter, query, db, negate = false) {
4114
4205
  if (isEntitiesSearchFilter(filter)) {
4115
4206
  return query.andWhere(function filterFunction() {
4116
- addCondition(this, db, filter);
4207
+ addCondition(this, db, filter, negate);
4117
4208
  });
4118
4209
  }
4119
- if (isOrEntityFilter(filter)) {
4120
- return query.andWhere(function filterFunction() {
4121
- var _a;
4210
+ if (isNegationEntityFilter(filter)) {
4211
+ return parseFilter(filter.not, query, db, !negate);
4212
+ }
4213
+ return query[negate ? "andWhereNot" : "andWhere"](function filterFunction() {
4214
+ var _a, _b;
4215
+ if (isOrEntityFilter(filter)) {
4122
4216
  for (const subFilter of (_a = filter.anyOf) != null ? _a : []) {
4123
4217
  this.orWhere((subQuery) => parseFilter(subFilter, subQuery, db));
4124
4218
  }
4125
- });
4126
- }
4127
- if (isAndEntityFilter(filter)) {
4128
- return query.andWhere(function filterFunction() {
4129
- var _a;
4130
- for (const subFilter of (_a = filter.allOf) != null ? _a : []) {
4219
+ } else {
4220
+ for (const subFilter of (_b = filter.allOf) != null ? _b : []) {
4131
4221
  this.andWhere((subQuery) => parseFilter(subFilter, subQuery, db));
4132
4222
  }
4133
- });
4134
- }
4135
- return query;
4223
+ }
4224
+ });
4136
4225
  }
4137
4226
  class NextEntitiesCatalog {
4138
4227
  constructor(database) {
@@ -4862,6 +4951,7 @@ class NextCatalogBuilder {
4862
4951
  return [
4863
4952
  new FileReaderProcessor(),
4864
4953
  BitbucketDiscoveryProcessor.fromConfig(config, {logger}),
4954
+ AzureDevOpsDiscoveryProcessor.fromConfig(config, {logger}),
4865
4955
  GithubDiscoveryProcessor.fromConfig(config, {logger}),
4866
4956
  GithubOrgReaderProcessor.fromConfig(config, {logger}),
4867
4957
  GitLabDiscoveryProcessor.fromConfig(config, {logger}),
@@ -5080,7 +5170,7 @@ class CatalogBuilder {
5080
5170
  new BuiltinKindsEntityProcessor()
5081
5171
  ];
5082
5172
  if (!this.processorsReplace) {
5083
- processors.push(new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, {logger}), GithubDiscoveryProcessor.fromConfig(config, {logger}), GithubOrgReaderProcessor.fromConfig(config, {logger}), GitLabDiscoveryProcessor.fromConfig(config, {logger}), new UrlReaderProcessor({reader, logger}), CodeOwnersProcessor.fromConfig(config, {logger, reader}), new LocationEntityProcessor({integrations}), new AnnotateLocationEntityProcessor({integrations}));
5173
+ processors.push(new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, {logger}), GithubDiscoveryProcessor.fromConfig(config, {logger}), AzureDevOpsDiscoveryProcessor.fromConfig(config, {logger}), GithubOrgReaderProcessor.fromConfig(config, {logger}), GitLabDiscoveryProcessor.fromConfig(config, {logger}), new UrlReaderProcessor({reader, logger}), CodeOwnersProcessor.fromConfig(config, {logger, reader}), new LocationEntityProcessor({integrations}), new AnnotateLocationEntityProcessor({integrations}));
5084
5174
  }
5085
5175
  processors.push(...this.processors);
5086
5176
  return processors;
@@ -5267,6 +5357,22 @@ class DefaultCatalogCollator {
5267
5357
  }
5268
5358
  return formatted.toLowerCase();
5269
5359
  }
5360
+ isUserEntity(entity) {
5361
+ return entity.kind.toLocaleUpperCase("en-US") === "USER";
5362
+ }
5363
+ getDocumentText(entity) {
5364
+ var _a, _b, _c, _d, _e, _f;
5365
+ let documentText = entity.metadata.description || "";
5366
+ if (this.isUserEntity(entity)) {
5367
+ if (((_b = (_a = entity.spec) == null ? void 0 : _a.profile) == null ? void 0 : _b.displayName) && documentText) {
5368
+ const displayName = (_d = (_c = entity.spec) == null ? void 0 : _c.profile) == null ? void 0 : _d.displayName;
5369
+ documentText = displayName.concat(" : ", documentText);
5370
+ } else {
5371
+ documentText = ((_f = (_e = entity.spec) == null ? void 0 : _e.profile) == null ? void 0 : _f.displayName) || documentText;
5372
+ }
5373
+ }
5374
+ return documentText;
5375
+ }
5270
5376
  async execute() {
5271
5377
  const response = await this.catalogClient.getEntities({
5272
5378
  filter: this.filter
@@ -5280,7 +5386,7 @@ class DefaultCatalogCollator {
5280
5386
  kind: entity.kind,
5281
5387
  name: entity.metadata.name
5282
5388
  }),
5283
- text: entity.metadata.description || "",
5389
+ text: this.getDocumentText(entity),
5284
5390
  componentType: ((_c = (_b = entity.spec) == null ? void 0 : _b.type) == null ? void 0 : _c.toString()) || "other",
5285
5391
  namespace: entity.metadata.namespace || "default",
5286
5392
  kind: entity.kind,
@@ -5295,6 +5401,7 @@ exports.AnnotateLocationEntityProcessor = AnnotateLocationEntityProcessor;
5295
5401
  exports.AnnotateScmSlugEntityProcessor = AnnotateScmSlugEntityProcessor;
5296
5402
  exports.AwsOrganizationCloudAccountProcessor = AwsOrganizationCloudAccountProcessor;
5297
5403
  exports.AwsS3DiscoveryProcessor = AwsS3DiscoveryProcessor;
5404
+ exports.AzureDevOpsDiscoveryProcessor = AzureDevOpsDiscoveryProcessor;
5298
5405
  exports.BitbucketDiscoveryProcessor = BitbucketDiscoveryProcessor;
5299
5406
  exports.BuiltinKindsEntityProcessor = BuiltinKindsEntityProcessor;
5300
5407
  exports.CatalogBuilder = CatalogBuilder;