@backstage/plugin-catalog-backend 3.5.0-next.1 → 3.5.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/config.d.ts +9 -0
  3. package/dist/database/operations/stitcher/getDeferredStitchableEntities.cjs.js +6 -6
  4. package/dist/database/operations/stitcher/getDeferredStitchableEntities.cjs.js.map +1 -1
  5. package/dist/database/operations/stitcher/markDeferredStitchCompleted.cjs.js +1 -4
  6. package/dist/database/operations/stitcher/markDeferredStitchCompleted.cjs.js.map +1 -1
  7. package/dist/database/operations/stitcher/markForStitching.cjs.js +19 -8
  8. package/dist/database/operations/stitcher/markForStitching.cjs.js.map +1 -1
  9. package/dist/database/operations/stitcher/performStitching.cjs.js +12 -8
  10. package/dist/database/operations/stitcher/performStitching.cjs.js.map +1 -1
  11. package/dist/ingestion/CatalogRules.cjs.js +6 -6
  12. package/dist/ingestion/CatalogRules.cjs.js.map +1 -1
  13. package/dist/permissions/rules/createPropertyRule.cjs.js +4 -4
  14. package/dist/permissions/rules/createPropertyRule.cjs.js.map +1 -1
  15. package/dist/permissions/rules/hasAnnotation.cjs.js +4 -4
  16. package/dist/permissions/rules/hasAnnotation.cjs.js.map +1 -1
  17. package/dist/permissions/rules/hasLabel.cjs.js +4 -4
  18. package/dist/permissions/rules/hasLabel.cjs.js.map +1 -1
  19. package/dist/permissions/rules/isEntityKind.cjs.js +3 -3
  20. package/dist/permissions/rules/isEntityKind.cjs.js.map +1 -1
  21. package/dist/permissions/rules/isEntityOwner.cjs.js +3 -3
  22. package/dist/permissions/rules/isEntityOwner.cjs.js.map +1 -1
  23. package/dist/providers/DefaultLocationStore.cjs.js +14 -2
  24. package/dist/providers/DefaultLocationStore.cjs.js.map +1 -1
  25. package/dist/schema/openapi/generated/router.cjs.js +19 -0
  26. package/dist/schema/openapi/generated/router.cjs.js.map +1 -1
  27. package/dist/service/AuthorizedLocationService.cjs.js.map +1 -1
  28. package/dist/service/CatalogBuilder.cjs.js +4 -1
  29. package/dist/service/CatalogBuilder.cjs.js.map +1 -1
  30. package/dist/service/DefaultEntitiesCatalog.cjs.js +43 -38
  31. package/dist/service/DefaultEntitiesCatalog.cjs.js.map +1 -1
  32. package/dist/service/DefaultLocationService.cjs.js +6 -3
  33. package/dist/service/DefaultLocationService.cjs.js.map +1 -1
  34. package/dist/service/createRouter.cjs.js +10 -8
  35. package/dist/service/createRouter.cjs.js.map +1 -1
  36. package/dist/service/util.cjs.js +21 -21
  37. package/dist/service/util.cjs.js.map +1 -1
  38. package/dist/stitching/progressTracker.cjs.js +3 -1
  39. package/dist/stitching/progressTracker.cjs.js.map +1 -1
  40. package/migrations/20260214000000_search_fk_final_entities.js +220 -36
  41. package/migrations/20260215000000_move_stitch_queue.js +174 -0
  42. package/package.json +21 -21
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 3.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a6b2819: Added `query-catalog-entities` action to the catalog backend actions registry. Supports predicate-based filtering with `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators.
8
+ - 972f686: Added support for predicate-based filtering on the `/entities/by-refs` endpoint via the `query` field in the request body. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators.
9
+ - 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error.
10
+ - 56c908e: Added support for predicate-based filtering on the `/entity-facets` endpoint via a new `POST` method. Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$contains`, and `$hasPrefix` operators.
11
+ - 0fbcf23: Migrated OpenAPI schemas to 3.1.
12
+ - bf71677: Added opentelemetry metrics for SCM events:
13
+
14
+ - `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`.
15
+
16
+ - 51e23eb: Added predicate-based entity filtering via POST /entities/by-query endpoint.
17
+
18
+ Supports `$all`, `$any`, `$not`, `$exists`, `$in`, `$hasPrefix`, and (partially) `$contains` operators for expressive entity queries. Integrated into the existing `queryEntities` flow with full cursor-based pagination, permission enforcement, and `totalItems` support.
19
+
20
+ The catalog client's `queryEntities()` method automatically routes to the POST endpoint when a `query` predicate is provided.
21
+
22
+ ### Patch Changes
23
+
24
+ - a91bd1b: Improved catalog entity deletion so parent invalidation and deferred relation restitch scheduling are coordinated more safely.
25
+ - 6738cf0: build(deps): bump `minimatch` from 9.0.5 to 10.2.1
26
+ - 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible.
27
+ - fbf382f: Minor internal optimisation
28
+ - 1ee5b28: Migrates existing catalog metrics to use the alpha MetricsService. This release is a 1:1 migration with no breaking changes.
29
+ - 72747b4: Deprecated two processors as they have been moved to the Community Plugins repo with their own backend modules:
30
+
31
+ - `AnnotateScmSlugEntityProcessor`: Use `@backstage-community/plugin-catalog-backend-module-annotate-scm-slug` instead
32
+ - `CodeOwnersProcessor`: Use `@backstage-community/plugin-catalog-backend-module-codeowners` instead
33
+
34
+ - 3644b72: Make the `search` foreign key catalog migration non-blocking on large tables by using batch deletes and PostgreSQL `NOT VALID`/`VALIDATE` to reduce lock duration
35
+ - a49a40d: Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports.
36
+ - 3181973: Changed the `search` table foreign key to point to `final_entities` instead of `refresh_state`
37
+ - Updated dependencies
38
+ - @backstage/backend-plugin-api@1.8.0
39
+ - @backstage/catalog-client@1.14.0
40
+ - @backstage/integration@2.0.0
41
+ - @backstage/plugin-catalog-node@2.1.0
42
+ - @backstage/filter-predicates@0.1.1
43
+ - @backstage/plugin-permission-common@0.9.7
44
+ - @backstage/plugin-permission-node@0.10.11
45
+ - @backstage/catalog-model@1.7.7
46
+ - @backstage/backend-openapi-utils@0.6.7
47
+ - @backstage/plugin-events-node@0.4.20
48
+
49
+ ## 3.5.0-next.2
50
+
51
+ ### Minor Changes
52
+
53
+ - 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error.
54
+
55
+ ### Patch Changes
56
+
57
+ - 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible.
58
+ - Updated dependencies
59
+ - @backstage/backend-plugin-api@1.8.0-next.1
60
+ - @backstage/catalog-client@1.14.0-next.2
61
+ - @backstage/integration@2.0.0-next.2
62
+ - @backstage/backend-openapi-utils@0.6.7-next.1
63
+ - @backstage/plugin-catalog-node@2.1.0-next.2
64
+ - @backstage/plugin-events-node@0.4.20-next.1
65
+ - @backstage/plugin-permission-node@0.10.11-next.1
66
+
3
67
  ## 3.5.0-next.1
4
68
 
5
69
  ### Minor Changes
package/config.d.ts CHANGED
@@ -191,6 +191,15 @@ export interface Config {
191
191
  stitchTimeout?: HumanDuration | string;
192
192
  };
193
193
 
194
+ /**
195
+ * The strategy to use when there is a conflict with a location being registered.
196
+ *
197
+ * The default value is "reject".
198
+ *
199
+ * The "refresh" strategy will refresh the existing location instead of throwing a conflict error.
200
+ */
201
+ defaultLocationConflictStrategy?: 'refresh' | 'reject';
202
+
194
203
  /**
195
204
  * The interval at which the catalog should process its entities.
196
205
  * @remarks
@@ -5,27 +5,27 @@ var conversion = require('../../conversion.cjs.js');
5
5
 
6
6
  async function getDeferredStitchableEntities(options) {
7
7
  const { knex, batchSize, stitchTimeout } = options;
8
- let itemsQuery = knex("refresh_state").select(
8
+ let itemsQuery = knex("stitch_queue").select(
9
9
  "entity_ref",
10
10
  "next_stitch_at",
11
- "next_stitch_ticket"
11
+ "stitch_ticket"
12
12
  );
13
13
  if (["mysql", "mysql2", "pg"].includes(knex.client.config.client)) {
14
14
  itemsQuery = itemsQuery.forUpdate().skipLocked();
15
15
  }
16
- const items = await itemsQuery.whereNotNull("next_stitch_at").whereNotNull("next_stitch_ticket").where("next_stitch_at", "<=", knex.fn.now()).orderBy("next_stitch_at", "asc").limit(batchSize);
16
+ const items = await itemsQuery.where("next_stitch_at", "<=", knex.fn.now()).orderBy("next_stitch_at", "asc").limit(batchSize);
17
17
  if (!items.length) {
18
18
  return [];
19
19
  }
20
- await knex("refresh_state").whereIn(
20
+ await knex("stitch_queue").whereIn(
21
21
  "entity_ref",
22
22
  items.map((i) => i.entity_ref)
23
- ).whereNotNull("next_stitch_ticket").update({
23
+ ).update({
24
24
  next_stitch_at: nowPlus(knex, stitchTimeout)
25
25
  });
26
26
  return items.map((i) => ({
27
27
  entityRef: i.entity_ref,
28
- stitchTicket: i.next_stitch_ticket,
28
+ stitchTicket: i.stitch_ticket,
29
29
  stitchRequestedAt: conversion.timestampToDateTime(i.next_stitch_at)
30
30
  }));
31
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"getDeferredStitchableEntities.cjs.js","sources":["../../../../src/database/operations/stitcher/getDeferredStitchableEntities.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { durationToMilliseconds, HumanDuration } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { timestampToDateTime } from '../../conversion';\nimport { DbRefreshStateRow } from '../../tables';\n\n// TODO(freben): There is no retry counter or similar. If items start\n// perpetually crashing during stitching, they'll just get silently retried over\n// and over again, for better or worse. This will be visible in metrics though.\n\n/**\n * Finds entities that are marked for deferred stitching.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * They are expected to already have the next_stitch_ticket set (by\n * markForStitching) so that their tickets can be returned with each item.\n *\n * All returned items have their next_stitch_at updated to be moved forward by\n * the given timeout duration. This has the effect that they will be picked up\n * for stitching again in the future, if it hasn't completed by that point for\n * some reason (restarts, crashes, etc).\n */\nexport async function getDeferredStitchableEntities(options: {\n knex: Knex | Knex.Transaction;\n batchSize: number;\n stitchTimeout: HumanDuration;\n}): Promise<\n Array<{\n entityRef: string;\n stitchTicket: string;\n stitchRequestedAt: DateTime; // the time BEFORE moving it forward by the timeout\n }>\n> {\n const { knex, batchSize, stitchTimeout } = options;\n\n let itemsQuery = knex<DbRefreshStateRow>('refresh_state').select(\n 'entity_ref',\n 'next_stitch_at',\n 'next_stitch_ticket',\n );\n\n // This avoids duplication of work because of race conditions and is\n // also fast because locked rows are ignored rather than blocking.\n // It's only available in MySQL and PostgreSQL\n if (['mysql', 'mysql2', 'pg'].includes(knex.client.config.client)) {\n itemsQuery = itemsQuery.forUpdate().skipLocked();\n }\n\n const items = await itemsQuery\n .whereNotNull('next_stitch_at')\n .whereNotNull('next_stitch_ticket')\n .where('next_stitch_at', '<=', knex.fn.now())\n .orderBy('next_stitch_at', 'asc')\n .limit(batchSize);\n\n if (!items.length) {\n return [];\n }\n\n await knex<DbRefreshStateRow>('refresh_state')\n .whereIn(\n 'entity_ref',\n items.map(i => i.entity_ref),\n )\n // avoid race condition where someone completes a stitch right between these statements\n .whereNotNull('next_stitch_ticket')\n .update({\n next_stitch_at: nowPlus(knex, stitchTimeout),\n });\n\n return items.map(i => ({\n entityRef: i.entity_ref,\n stitchTicket: i.next_stitch_ticket!,\n stitchRequestedAt: timestampToDateTime(i.next_stitch_at!),\n }));\n}\n\nfunction nowPlus(knex: Knex, duration: HumanDuration): Knex.Raw {\n const seconds = durationToMilliseconds(duration) / 1000;\n if (knex.client.config.client.includes('sqlite3')) {\n return knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]);\n } else if (knex.client.config.client.includes('mysql')) {\n return knex.raw(`now() + interval ${seconds} second`);\n }\n return knex.raw(`now() + interval '${seconds} seconds'`);\n}\n"],"names":["timestampToDateTime","durationToMilliseconds"],"mappings":";;;;;AAyCA,eAAsB,8BAA8B,OAAA,EAUlD;AACA,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,aAAA,EAAc,GAAI,OAAA;AAE3C,EAAA,IAAI,UAAA,GAAa,IAAA,CAAwB,eAAe,CAAA,CAAE,MAAA;AAAA,IACxD,YAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF;AAKA,EAAA,IAAI,CAAC,OAAA,EAAS,QAAA,EAAU,IAAI,CAAA,CAAE,SAAS,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,EAAG;AACjE,IAAA,UAAA,GAAa,UAAA,CAAW,SAAA,EAAU,CAAE,UAAA,EAAW;AAAA,EACjD;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAM,UAAA,CACjB,YAAA,CAAa,gBAAgB,CAAA,CAC7B,YAAA,CAAa,oBAAoB,CAAA,CACjC,KAAA,CAAM,gBAAA,EAAkB,MAAM,IAAA,CAAK,EAAA,CAAG,KAAK,CAAA,CAC3C,QAAQ,gBAAA,EAAkB,KAAK,CAAA,CAC/B,KAAA,CAAM,SAAS,CAAA;AAElB,EAAA,IAAI,CAAC,MAAM,MAAA,EAAQ;AACjB,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,IAAA,CAAwB,eAAe,CAAA,CAC1C,OAAA;AAAA,IACC,YAAA;AAAA,IACA,KAAA,CAAM,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,UAAU;AAAA,GAC7B,CAEC,YAAA,CAAa,oBAAoB,CAAA,CACjC,MAAA,CAAO;AAAA,IACN,cAAA,EAAgB,OAAA,CAAQ,IAAA,EAAM,aAAa;AAAA,GAC5C,CAAA;AAEH,EAAA,OAAO,KAAA,CAAM,IAAI,CAAA,CAAA,MAAM;AAAA,IACrB,WAAW,CAAA,CAAE,UAAA;AAAA,IACb,cAAc,CAAA,CAAE,kBAAA;AAAA,IAChB,iBAAA,EAAmBA,8BAAA,CAAoB,CAAA,CAAE,cAAe;AAAA,GAC1D,CAAE,CAAA;AACJ;AAEA,SAAS,OAAA,CAAQ,MAAY,QAAA,EAAmC;AAC9D,EAAA,MAAM,OAAA,GAAUC,4BAAA,CAAuB,QAAQ,CAAA,GAAI,GAAA;AACnD,EAAA,IAAI,KAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,SAAS,CAAA,EAAG;AACjD,IAAA,OAAO,KAAK,GAAA,CAAI,CAAA,kBAAA,CAAA,EAAsB,CAAC,CAAA,EAAG,OAAO,UAAU,CAAC,CAAA;AAAA,EAC9D,WAAW,IAAA,CAAK,MAAA,CAAO,OAAO,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACtD,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,iBAAA,EAAoB,OAAO,CAAA,OAAA,CAAS,CAAA;AAAA,EACtD;AACA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,kBAAA,EAAqB,OAAO,CAAA,SAAA,CAAW,CAAA;AACzD;;;;"}
1
+ {"version":3,"file":"getDeferredStitchableEntities.cjs.js","sources":["../../../../src/database/operations/stitcher/getDeferredStitchableEntities.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { durationToMilliseconds, HumanDuration } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { timestampToDateTime } from '../../conversion';\nimport { DbStitchQueueRow } from '../../tables';\n\n// TODO(freben): There is no retry counter or similar. If items start\n// perpetually crashing during stitching, they'll just get silently retried over\n// and over again, for better or worse. This will be visible in metrics though.\n\n/**\n * Finds entities that are marked for deferred stitching.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * They are expected to already have the stitch_ticket set (by\n * markForStitching) so that their tickets can be returned with each item.\n *\n * All returned items have their next_stitch_at updated to be moved forward by\n * the given timeout duration. This has the effect that they will be picked up\n * for stitching again in the future, if it hasn't completed by that point for\n * some reason (restarts, crashes, etc).\n */\nexport async function getDeferredStitchableEntities(options: {\n knex: Knex | Knex.Transaction;\n batchSize: number;\n stitchTimeout: HumanDuration;\n}): Promise<\n Array<{\n entityRef: string;\n stitchTicket: string;\n stitchRequestedAt: DateTime; // the time BEFORE moving it forward by the timeout\n }>\n> {\n const { knex, batchSize, stitchTimeout } = options;\n\n let itemsQuery = knex<DbStitchQueueRow>('stitch_queue').select(\n 'entity_ref',\n 'next_stitch_at',\n 'stitch_ticket',\n );\n\n // This avoids duplication of work because of race conditions and is\n // also fast because locked rows are ignored rather than blocking.\n // It's only available in MySQL and PostgreSQL\n if (['mysql', 'mysql2', 'pg'].includes(knex.client.config.client)) {\n itemsQuery = itemsQuery.forUpdate().skipLocked();\n }\n\n const items = await itemsQuery\n .where('next_stitch_at', '<=', knex.fn.now())\n .orderBy('next_stitch_at', 'asc')\n .limit(batchSize);\n\n if (!items.length) {\n return [];\n }\n\n await knex<DbStitchQueueRow>('stitch_queue')\n .whereIn(\n 'entity_ref',\n items.map(i => i.entity_ref),\n )\n .update({\n next_stitch_at: nowPlus(knex, stitchTimeout),\n });\n\n return items.map(i => ({\n entityRef: i.entity_ref,\n stitchTicket: i.stitch_ticket,\n stitchRequestedAt: timestampToDateTime(i.next_stitch_at),\n }));\n}\n\nfunction nowPlus(knex: Knex, duration: HumanDuration): Knex.Raw {\n const seconds = durationToMilliseconds(duration) / 1000;\n if (knex.client.config.client.includes('sqlite3')) {\n return knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]);\n } else if (knex.client.config.client.includes('mysql')) {\n return knex.raw(`now() + interval ${seconds} second`);\n }\n return knex.raw(`now() + interval '${seconds} seconds'`);\n}\n"],"names":["timestampToDateTime","durationToMilliseconds"],"mappings":";;;;;AAyCA,eAAsB,8BAA8B,OAAA,EAUlD;AACA,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,aAAA,EAAc,GAAI,OAAA;AAE3C,EAAA,IAAI,UAAA,GAAa,IAAA,CAAuB,cAAc,CAAA,CAAE,MAAA;AAAA,IACtD,YAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF;AAKA,EAAA,IAAI,CAAC,OAAA,EAAS,QAAA,EAAU,IAAI,CAAA,CAAE,SAAS,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,EAAG;AACjE,IAAA,UAAA,GAAa,UAAA,CAAW,SAAA,EAAU,CAAE,UAAA,EAAW;AAAA,EACjD;AAEA,EAAA,MAAM,QAAQ,MAAM,UAAA,CACjB,KAAA,CAAM,gBAAA,EAAkB,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,EAAK,EAC3C,OAAA,CAAQ,gBAAA,EAAkB,KAAK,CAAA,CAC/B,MAAM,SAAS,CAAA;AAElB,EAAA,IAAI,CAAC,MAAM,MAAA,EAAQ;AACjB,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,OAAA;AAAA,IACC,YAAA;AAAA,IACA,KAAA,CAAM,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,UAAU;AAAA,IAE5B,MAAA,CAAO;AAAA,IACN,cAAA,EAAgB,OAAA,CAAQ,IAAA,EAAM,aAAa;AAAA,GAC5C,CAAA;AAEH,EAAA,OAAO,KAAA,CAAM,IAAI,CAAA,CAAA,MAAM;AAAA,IACrB,WAAW,CAAA,CAAE,UAAA;AAAA,IACb,cAAc,CAAA,CAAE,aAAA;AAAA,IAChB,iBAAA,EAAmBA,8BAAA,CAAoB,CAAA,CAAE,cAAc;AAAA,GACzD,CAAE,CAAA;AACJ;AAEA,SAAS,OAAA,CAAQ,MAAY,QAAA,EAAmC;AAC9D,EAAA,MAAM,OAAA,GAAUC,4BAAA,CAAuB,QAAQ,CAAA,GAAI,GAAA;AACnD,EAAA,IAAI,KAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,SAAS,CAAA,EAAG;AACjD,IAAA,OAAO,KAAK,GAAA,CAAI,CAAA,kBAAA,CAAA,EAAsB,CAAC,CAAA,EAAG,OAAO,UAAU,CAAC,CAAA;AAAA,EAC9D,WAAW,IAAA,CAAK,MAAA,CAAO,OAAO,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACtD,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,iBAAA,EAAoB,OAAO,CAAA,OAAA,CAAS,CAAA;AAAA,EACtD;AACA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,kBAAA,EAAqB,OAAO,CAAA,SAAA,CAAW,CAAA;AACzD;;;;"}
@@ -2,10 +2,7 @@
2
2
 
3
3
  async function markDeferredStitchCompleted(option) {
4
4
  const { knex, entityRef, stitchTicket } = option;
5
- await knex("refresh_state").update({
6
- next_stitch_at: null,
7
- next_stitch_ticket: null
8
- }).where("entity_ref", "=", entityRef).andWhere("next_stitch_ticket", "=", stitchTicket);
5
+ await knex("stitch_queue").where("entity_ref", "=", entityRef).andWhere("stitch_ticket", "=", stitchTicket).delete();
9
6
  }
10
7
 
11
8
  exports.markDeferredStitchCompleted = markDeferredStitchCompleted;
@@ -1 +1 @@
1
- {"version":3,"file":"markDeferredStitchCompleted.cjs.js","sources":["../../../../src/database/operations/stitcher/markDeferredStitchCompleted.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport { DbRefreshStateRow } from '../../tables';\n\n/**\n * Marks a single entity as having been stitched.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * The timestamp and ticket are only reset if the ticket hasn't changed. If it\n * has, it means that a new stitch request has been made, and the entity should\n * be stitched once more some time in the future - or is indeed already being\n * stitched concurrently with ourselves.\n */\nexport async function markDeferredStitchCompleted(option: {\n knex: Knex | Knex.Transaction;\n entityRef: string;\n stitchTicket: string;\n}): Promise<void> {\n const { knex, entityRef, stitchTicket } = option;\n\n await knex<DbRefreshStateRow>('refresh_state')\n .update({\n next_stitch_at: null,\n next_stitch_ticket: null,\n })\n .where('entity_ref', '=', entityRef)\n .andWhere('next_stitch_ticket', '=', stitchTicket);\n}\n"],"names":[],"mappings":";;AA+BA,eAAsB,4BAA4B,MAAA,EAIhC;AAChB,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,YAAA,EAAa,GAAI,MAAA;AAE1C,EAAA,MAAM,IAAA,CAAwB,eAAe,CAAA,CAC1C,MAAA,CAAO;AAAA,IACN,cAAA,EAAgB,IAAA;AAAA,IAChB,kBAAA,EAAoB;AAAA,GACrB,CAAA,CACA,KAAA,CAAM,YAAA,EAAc,GAAA,EAAK,SAAS,CAAA,CAClC,QAAA,CAAS,oBAAA,EAAsB,GAAA,EAAK,YAAY,CAAA;AACrD;;;;"}
1
+ {"version":3,"file":"markDeferredStitchCompleted.cjs.js","sources":["../../../../src/database/operations/stitcher/markDeferredStitchCompleted.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport { DbStitchQueueRow } from '../../tables';\n\n/**\n * Marks a single entity as having been stitched.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * The row is only deleted from stitch_queue if the ticket hasn't changed. If\n * it has, it means that a new stitch request has been made, and the entity\n * should be stitched once more some time in the future - or is indeed already\n * being stitched concurrently with ourselves.\n */\nexport async function markDeferredStitchCompleted(option: {\n knex: Knex | Knex.Transaction;\n entityRef: string;\n stitchTicket: string;\n}): Promise<void> {\n const { knex, entityRef, stitchTicket } = option;\n\n await knex<DbStitchQueueRow>('stitch_queue')\n .where('entity_ref', '=', entityRef)\n .andWhere('stitch_ticket', '=', stitchTicket)\n .delete();\n}\n"],"names":[],"mappings":";;AA+BA,eAAsB,4BAA4B,MAAA,EAIhC;AAChB,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,YAAA,EAAa,GAAI,MAAA;AAE1C,EAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,KAAA,CAAM,YAAA,EAAc,GAAA,EAAK,SAAS,CAAA,CAClC,QAAA,CAAS,eAAA,EAAiB,GAAA,EAAK,YAAY,EAC3C,MAAA,EAAO;AACZ;;;;"}
@@ -41,18 +41,29 @@ async function markForStitching(options) {
41
41
  const ticket = uuid.v4();
42
42
  for (const chunk of entityRefs) {
43
43
  await util.retryOnDeadlock(async () => {
44
- await knex("refresh_state").update({
45
- next_stitch_at: knex.fn.now(),
46
- next_stitch_ticket: ticket
47
- }).whereIn("entity_ref", chunk);
44
+ if (chunk.length > 0) {
45
+ await knex("stitch_queue").insert(
46
+ chunk.map((ref) => ({
47
+ entity_ref: ref,
48
+ stitch_ticket: ticket,
49
+ next_stitch_at: knex.fn.now()
50
+ }))
51
+ ).onConflict("entity_ref").merge(["next_stitch_at", "stitch_ticket"]);
52
+ }
48
53
  }, knex);
49
54
  }
50
55
  for (const chunk of entityIds) {
51
56
  await util.retryOnDeadlock(async () => {
52
- await knex("refresh_state").update({
53
- next_stitch_at: knex.fn.now(),
54
- next_stitch_ticket: ticket
55
- }).whereIn("entity_id", chunk);
57
+ const refreshStateRows = await knex("refresh_state").select("entity_ref").whereIn("entity_id", chunk);
58
+ if (refreshStateRows.length > 0) {
59
+ await knex("stitch_queue").insert(
60
+ refreshStateRows.map((row) => ({
61
+ entity_ref: row.entity_ref,
62
+ stitch_ticket: ticket,
63
+ next_stitch_at: knex.fn.now()
64
+ }))
65
+ ).onConflict("entity_ref").merge(["next_stitch_at", "stitch_ticket"]);
66
+ }
56
67
  }, knex);
57
68
  }
58
69
  } else {
@@ -1 +1 @@
1
- {"version":3,"file":"markForStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/markForStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport splitToChunks from 'lodash/chunk';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport { DbFinalEntitiesRow, DbRefreshStateRow } from '../../tables';\nimport { retryOnDeadlock } from '../../util';\n\nconst UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention\n\n/**\n * Marks a number of entities for stitching some time in the near\n * future.\n *\n * @remarks\n */\nexport async function markForStitching(options: {\n knex: Knex | Knex.Transaction;\n strategy: StitchingStrategy;\n entityRefs?: Iterable<string>;\n entityIds?: Iterable<string>;\n}): Promise<void> {\n const entityRefs = sortSplit(options.entityRefs);\n const entityIds = sortSplit(options.entityIds);\n const knex = options.knex;\n const mode = options.strategy.mode;\n\n if (mode === 'immediate') {\n for (const chunk of entityRefs) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_ref', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_ref', chunk);\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_id', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_id', chunk);\n }, knex);\n }\n } else if (mode === 'deferred') {\n // It's OK that this is shared across refresh state rows; it just needs to\n // be uniquely generated for every new stitch request.\n const ticket = uuid();\n\n // Update by primary key in deterministic order to avoid deadlocks\n for (const chunk of entityRefs) {\n await retryOnDeadlock(async () => {\n await knex<DbRefreshStateRow>('refresh_state')\n .update({\n next_stitch_at: knex.fn.now(),\n next_stitch_ticket: ticket,\n })\n .whereIn('entity_ref', chunk);\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await retryOnDeadlock(async () => {\n await knex<DbRefreshStateRow>('refresh_state')\n .update({\n next_stitch_at: knex.fn.now(),\n next_stitch_ticket: ticket,\n })\n .whereIn('entity_id', chunk);\n }, knex);\n }\n } else {\n throw new Error(`Unknown stitching strategy mode ${mode}`);\n }\n}\n\nfunction sortSplit(input: Iterable<string> | undefined): string[][] {\n if (!input) {\n return [];\n }\n const array = Array.isArray(input) ? input.slice() : [...input];\n array.sort();\n return splitToChunks(array, UPDATE_CHUNK_SIZE);\n}\n"],"names":["retryOnDeadlock","uuid","splitToChunks"],"mappings":"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"}
1
+ {"version":3,"file":"markForStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/markForStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport splitToChunks from 'lodash/chunk';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbStitchQueueRow,\n} from '../../tables';\nimport { retryOnDeadlock } from '../../util';\n\nconst UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention\n\n/**\n * Marks a number of entities for stitching some time in the near\n * future.\n *\n * @remarks\n */\nexport async function markForStitching(options: {\n knex: Knex | Knex.Transaction;\n strategy: StitchingStrategy;\n entityRefs?: Iterable<string>;\n entityIds?: Iterable<string>;\n}): Promise<void> {\n const entityRefs = sortSplit(options.entityRefs);\n const entityIds = sortSplit(options.entityIds);\n const knex = options.knex;\n const mode = options.strategy.mode;\n\n if (mode === 'immediate') {\n for (const chunk of entityRefs) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_ref', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_ref', chunk);\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_id', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_id', chunk);\n }, knex);\n }\n } else if (mode === 'deferred') {\n // It's OK that this is shared across stitch_queue rows; it just needs to\n // be uniquely generated for every new stitch request.\n const ticket = uuid();\n\n for (const chunk of entityRefs) {\n await retryOnDeadlock(async () => {\n if (chunk.length > 0) {\n await knex<DbStitchQueueRow>('stitch_queue')\n .insert(\n chunk.map(ref => ({\n entity_ref: ref,\n stitch_ticket: ticket,\n next_stitch_at: knex.fn.now(),\n })),\n )\n .onConflict('entity_ref')\n .merge(['next_stitch_at', 'stitch_ticket']);\n }\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await retryOnDeadlock(async () => {\n // Look up entity_refs from refresh_state by entity_id\n const refreshStateRows = await knex<DbRefreshStateRow>('refresh_state')\n .select('entity_ref')\n .whereIn('entity_id', chunk);\n\n if (refreshStateRows.length > 0) {\n await knex<DbStitchQueueRow>('stitch_queue')\n .insert(\n refreshStateRows.map(row => ({\n entity_ref: row.entity_ref,\n stitch_ticket: ticket,\n next_stitch_at: knex.fn.now(),\n })),\n )\n .onConflict('entity_ref')\n .merge(['next_stitch_at', 'stitch_ticket']);\n }\n }, knex);\n }\n } else {\n throw new Error(`Unknown stitching strategy mode ${mode}`);\n }\n}\n\nfunction sortSplit(input: Iterable<string> | undefined): string[][] {\n if (!input) {\n return [];\n }\n const array = Array.isArray(input) ? input.slice() : [...input];\n array.sort();\n return splitToChunks(array, UPDATE_CHUNK_SIZE);\n}\n"],"names":["retryOnDeadlock","uuid","splitToChunks"],"mappings":"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"}
@@ -2,7 +2,6 @@
2
2
 
3
3
  var catalogClient = require('@backstage/catalog-client');
4
4
  var catalogModel = require('@backstage/catalog-model');
5
- var uuid = require('uuid');
6
5
  var buildEntitySearch = require('./buildEntitySearch.cjs.js');
7
6
  var markDeferredStitchCompleted = require('./markDeferredStitchCompleted.cjs.js');
8
7
  var util = require('./util.cjs.js');
@@ -14,7 +13,7 @@ const scriptProtocolPattern = (
14
13
  );
15
14
  async function performStitching(options) {
16
15
  const { knex, logger, entityRef } = options;
17
- const stitchTicket = options.stitchTicket ?? uuid.v4();
16
+ const stitchTicket = options.stitchTicket;
18
17
  let removeFromStitchQueueOnCompletion = options.strategy.mode === "deferred";
19
18
  try {
20
19
  const entityResult = await knex("refresh_state").where({ entity_ref: entityRef }).limit(1).select("entity_id");
@@ -25,9 +24,8 @@ async function performStitching(options) {
25
24
  await knex("final_entities").insert({
26
25
  entity_id: entityResult[0].entity_id,
27
26
  hash: "",
28
- entity_ref: entityRef,
29
- stitch_ticket: stitchTicket
30
- }).onConflict("entity_id").merge(["stitch_ticket"]);
27
+ entity_ref: entityRef
28
+ }).onConflict("entity_id").ignore();
31
29
  } catch (error) {
32
30
  if (backendPluginApi.isDatabaseConflictError(error)) {
33
31
  logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);
@@ -121,11 +119,17 @@ async function performStitching(options) {
121
119
  entity.metadata.etag = hash;
122
120
  }
123
121
  const searchEntries = buildEntitySearch.buildEntitySearch(entityId, entity);
124
- const amountOfRowsChanged = await knex("final_entities").update({
122
+ let updateQuery = knex("final_entities").update({
125
123
  final_entity: JSON.stringify(entity),
126
124
  hash,
127
125
  last_updated_at: knex.fn.now()
128
- }).where("entity_id", entityId).where("stitch_ticket", stitchTicket);
126
+ }).where("entity_id", entityId);
127
+ if (options.strategy.mode === "deferred" && stitchTicket) {
128
+ updateQuery = updateQuery.whereExists(
129
+ knex("stitch_queue").where("stitch_queue.entity_ref", entityRef).where("stitch_queue.stitch_ticket", stitchTicket).select(knex.raw("1"))
130
+ );
131
+ }
132
+ const amountOfRowsChanged = await updateQuery;
129
133
  if (amountOfRowsChanged === 0) {
130
134
  logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);
131
135
  return "abandoned";
@@ -139,7 +143,7 @@ async function performStitching(options) {
139
143
  removeFromStitchQueueOnCompletion = false;
140
144
  throw error;
141
145
  } finally {
142
- if (removeFromStitchQueueOnCompletion) {
146
+ if (removeFromStitchQueueOnCompletion && stitchTicket) {
143
147
  await markDeferredStitchCompleted.markDeferredStitchCompleted({
144
148
  knex,
145
149
  entityRef,
@@ -1 +1 @@
1
- {"version":3,"file":"performStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/performStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';\nimport {\n ANNOTATION_EDIT_URL,\n ANNOTATION_VIEW_URL,\n EntityRelation,\n} from '@backstage/catalog-model';\nimport { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha';\nimport { SerializedError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../../tables';\nimport { buildEntitySearch } from './buildEntitySearch';\nimport { markDeferredStitchCompleted } from './markDeferredStitchCompleted';\nimport { BATCH_SIZE, generateStableHash } from './util';\nimport {\n LoggerService,\n isDatabaseConflictError,\n} from '@backstage/backend-plugin-api';\n\n// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js\nconst scriptProtocolPattern =\n // eslint-disable-next-line no-control-regex\n /^[\\u0000-\\u001F ]*j[\\r\\n\\t]*a[\\r\\n\\t]*v[\\r\\n\\t]*a[\\r\\n\\t]*s[\\r\\n\\t]*c[\\r\\n\\t]*r[\\r\\n\\t]*i[\\r\\n\\t]*p[\\r\\n\\t]*t[\\r\\n\\t]*\\:/i;\n\n/**\n * Performs the act of stitching - to take all of the various outputs from the\n * ingestion process, and stitching them together into the final entity JSON\n * shape.\n */\nexport async function performStitching(options: {\n knex: Knex | Knex.Transaction;\n logger: LoggerService;\n strategy: StitchingStrategy;\n entityRef: string;\n stitchTicket?: string;\n}): Promise<'changed' | 'unchanged' | 'abandoned'> {\n const { knex, logger, entityRef } = options;\n const stitchTicket = options.stitchTicket ?? uuid();\n\n // In deferred mode, the entity is removed from the stitch queue on ANY\n // completion, except when an exception is thrown. In the latter case, the\n // entity will be retried at a later time.\n let removeFromStitchQueueOnCompletion = options.strategy.mode === 'deferred';\n\n try {\n const entityResult = await knex<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .limit(1)\n .select('entity_id');\n if (!entityResult.length) {\n // Entity does no exist in refresh state table, no stitching required.\n return 'abandoned';\n }\n\n // Insert stitching ticket that will be compared before inserting the final entity.\n try {\n await knex<DbFinalEntitiesRow>('final_entities')\n .insert({\n entity_id: entityResult[0].entity_id,\n hash: '',\n entity_ref: entityRef,\n stitch_ticket: stitchTicket,\n })\n .onConflict('entity_id')\n .merge(['stitch_ticket']);\n } catch (error) {\n // It's possible to hit a race where a refresh_state table delete + insert\n // is done just after we read the entity_id from it. This conflict is safe\n // to ignore because the current stitching operation will be triggered by\n // the old entry, and the new entry will trigger it's own stitching that\n // will update the entity.\n if (isDatabaseConflictError(error)) {\n logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);\n return 'abandoned';\n }\n\n throw error;\n }\n\n // Selecting from refresh_state and final_entities should yield exactly\n // one row (except in abnormal cases where the stitch was invoked for\n // something that didn't exist at all, in which case it's zero rows).\n // The join with the temporary incoming_references still gives one row.\n const [processedResult, relationsResult] = await Promise.all([\n knex\n .with('incoming_references', function incomingReferences(builder) {\n return builder\n .from('refresh_state_references')\n .where({ target_entity_ref: entityRef })\n .count({ count: '*' });\n })\n .select({\n entityId: 'refresh_state.entity_id',\n processedEntity: 'refresh_state.processed_entity',\n errors: 'refresh_state.errors',\n incomingReferenceCount: 'incoming_references.count',\n previousHash: 'final_entities.hash',\n })\n .from('refresh_state')\n .where({ 'refresh_state.entity_ref': entityRef })\n .crossJoin(knex.raw('incoming_references'))\n .leftOuterJoin('final_entities', {\n 'final_entities.entity_id': 'refresh_state.entity_id',\n }),\n knex\n .distinct({\n relationType: 'type',\n relationTarget: 'target_entity_ref',\n })\n .from('relations')\n .where({ source_entity_ref: entityRef })\n .orderBy('relationType', 'asc')\n .orderBy('relationTarget', 'asc'),\n ]);\n\n // If there were no rows returned, it would mean that there was no\n // matching row even in the refresh_state. This can happen for example\n // if we emit a relation to something that hasn't been ingested yet.\n // It's safe to ignore this stitch attempt in that case.\n if (!processedResult.length) {\n logger.debug(\n `Unable to stitch ${entityRef}, item does not exist in refresh state table`,\n );\n return 'abandoned';\n }\n\n const {\n entityId,\n processedEntity,\n errors,\n incomingReferenceCount,\n previousHash,\n } = processedResult[0];\n\n // If there was no processed entity in place, the target hasn't been\n // through the processing steps yet. It's safe to ignore this stitch\n // attempt in that case, since another stitch will be triggered when\n // that processing has finished.\n if (!processedEntity) {\n logger.debug(\n `Unable to stitch ${entityRef}, the entity has not yet been processed`,\n );\n return 'abandoned';\n }\n\n // Grab the processed entity and stitch all of the relevant data into\n // it\n const entity = JSON.parse(processedEntity) as AlphaEntity;\n const isOrphan = Number(incomingReferenceCount) === 0;\n let statusItems: EntityStatusItem[] = [];\n\n if (isOrphan) {\n logger.debug(`${entityRef} is an orphan`);\n entity.metadata.annotations = {\n ...entity.metadata.annotations,\n ['backstage.io/orphan']: 'true',\n };\n }\n if (errors) {\n const parsedErrors = JSON.parse(errors) as SerializedError[];\n if (Array.isArray(parsedErrors) && parsedErrors.length) {\n statusItems = parsedErrors.map(e => ({\n type: ENTITY_STATUS_CATALOG_PROCESSING_TYPE,\n level: 'error',\n message: `${e.name}: ${e.message}`,\n error: e,\n }));\n }\n }\n // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors\n for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) {\n const value = entity.metadata.annotations?.[annotation];\n if (typeof value === 'string' && scriptProtocolPattern.test(value)) {\n entity.metadata.annotations![annotation] =\n 'https://backstage.io/annotation-rejected-for-security-reasons';\n }\n }\n\n // TODO: entityRef is lower case and should be uppercase in the final\n // result\n entity.relations = relationsResult\n .filter(row => row.relationType /* exclude null row, if relevant */)\n .map<EntityRelation>(row => ({\n type: row.relationType!,\n targetRef: row.relationTarget!,\n }));\n if (statusItems.length) {\n entity.status = {\n ...entity.status,\n items: [...(entity.status?.items ?? []), ...statusItems],\n };\n }\n\n // If the output entity was actually not changed, just abort\n const hash = generateStableHash(entity);\n if (hash === previousHash) {\n logger.debug(`Skipped stitching of ${entityRef}, no changes`);\n return 'unchanged';\n }\n\n entity.metadata.uid = entityId;\n if (!entity.metadata.etag) {\n // If the original data source did not have its own etag handling,\n // use the hash as a good-quality etag\n entity.metadata.etag = hash;\n }\n\n // This may throw if the entity is invalid, so we call it before\n // the final_entities write, even though we may end up not needing\n // to write the search index.\n const searchEntries = buildEntitySearch(entityId, entity);\n\n const amountOfRowsChanged = await knex<DbFinalEntitiesRow>('final_entities')\n .update({\n final_entity: JSON.stringify(entity),\n hash,\n last_updated_at: knex.fn.now(),\n })\n .where('entity_id', entityId)\n .where('stitch_ticket', stitchTicket);\n\n if (amountOfRowsChanged === 0) {\n logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);\n return 'abandoned';\n }\n\n await knex.transaction(async trx => {\n await trx<DbSearchRow>('search').where({ entity_id: entityId }).delete();\n await trx.batchInsert('search', searchEntries, BATCH_SIZE);\n });\n\n return 'changed';\n } catch (error) {\n removeFromStitchQueueOnCompletion = false;\n throw error;\n } finally {\n if (removeFromStitchQueueOnCompletion) {\n await markDeferredStitchCompleted({\n knex: knex,\n entityRef,\n stitchTicket,\n });\n }\n }\n}\n"],"names":["uuid","isDatabaseConflictError","ENTITY_STATUS_CATALOG_PROCESSING_TYPE","ANNOTATION_VIEW_URL","ANNOTATION_EDIT_URL","generateStableHash","buildEntitySearch","BATCH_SIZE","markDeferredStitchCompleted"],"mappings":"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"}
1
+ {"version":3,"file":"performStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/performStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';\nimport {\n ANNOTATION_EDIT_URL,\n ANNOTATION_VIEW_URL,\n EntityRelation,\n} from '@backstage/catalog-model';\nimport { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha';\nimport { SerializedError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbSearchRow,\n DbStitchQueueRow,\n} from '../../tables';\nimport { buildEntitySearch } from './buildEntitySearch';\nimport { markDeferredStitchCompleted } from './markDeferredStitchCompleted';\nimport { BATCH_SIZE, generateStableHash } from './util';\nimport {\n LoggerService,\n isDatabaseConflictError,\n} from '@backstage/backend-plugin-api';\n\n// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js\nconst scriptProtocolPattern =\n // eslint-disable-next-line no-control-regex\n /^[\\u0000-\\u001F ]*j[\\r\\n\\t]*a[\\r\\n\\t]*v[\\r\\n\\t]*a[\\r\\n\\t]*s[\\r\\n\\t]*c[\\r\\n\\t]*r[\\r\\n\\t]*i[\\r\\n\\t]*p[\\r\\n\\t]*t[\\r\\n\\t]*\\:/i;\n\n/**\n * Performs the act of stitching - to take all of the various outputs from the\n * ingestion process, and stitching them together into the final entity JSON\n * shape.\n */\nexport async function performStitching(options: {\n knex: Knex | Knex.Transaction;\n logger: LoggerService;\n strategy: StitchingStrategy;\n entityRef: string;\n stitchTicket?: string;\n}): Promise<'changed' | 'unchanged' | 'abandoned'> {\n const { knex, logger, entityRef } = options;\n const stitchTicket = options.stitchTicket;\n\n // In deferred mode, the entity is removed from the stitch queue on ANY\n // completion, except when an exception is thrown. In the latter case, the\n // entity will be retried at a later time.\n let removeFromStitchQueueOnCompletion = options.strategy.mode === 'deferred';\n\n try {\n const entityResult = await knex<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .limit(1)\n .select('entity_id');\n if (!entityResult.length) {\n // Entity does no exist in refresh state table, no stitching required.\n return 'abandoned';\n }\n\n // Ensure that a final_entities row exists for this entity.\n try {\n await knex<DbFinalEntitiesRow>('final_entities')\n .insert({\n entity_id: entityResult[0].entity_id,\n hash: '',\n entity_ref: entityRef,\n })\n .onConflict('entity_id')\n .ignore();\n } catch (error) {\n // It's possible to hit a race where a refresh_state table delete + insert\n // is done just after we read the entity_id from it. This conflict is safe\n // to ignore because the current stitching operation will be triggered by\n // the old entry, and the new entry will trigger it's own stitching that\n // will update the entity.\n if (isDatabaseConflictError(error)) {\n logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);\n return 'abandoned';\n }\n\n throw error;\n }\n\n // Selecting from refresh_state and final_entities should yield exactly\n // one row (except in abnormal cases where the stitch was invoked for\n // something that didn't exist at all, in which case it's zero rows).\n // The join with the temporary incoming_references still gives one row.\n const [processedResult, relationsResult] = await Promise.all([\n knex\n .with('incoming_references', function incomingReferences(builder) {\n return builder\n .from('refresh_state_references')\n .where({ target_entity_ref: entityRef })\n .count({ count: '*' });\n })\n .select({\n entityId: 'refresh_state.entity_id',\n processedEntity: 'refresh_state.processed_entity',\n errors: 'refresh_state.errors',\n incomingReferenceCount: 'incoming_references.count',\n previousHash: 'final_entities.hash',\n })\n .from('refresh_state')\n .where({ 'refresh_state.entity_ref': entityRef })\n .crossJoin(knex.raw('incoming_references'))\n .leftOuterJoin('final_entities', {\n 'final_entities.entity_id': 'refresh_state.entity_id',\n }),\n knex\n .distinct({\n relationType: 'type',\n relationTarget: 'target_entity_ref',\n })\n .from('relations')\n .where({ source_entity_ref: entityRef })\n .orderBy('relationType', 'asc')\n .orderBy('relationTarget', 'asc'),\n ]);\n\n // If there were no rows returned, it would mean that there was no\n // matching row even in the refresh_state. This can happen for example\n // if we emit a relation to something that hasn't been ingested yet.\n // It's safe to ignore this stitch attempt in that case.\n if (!processedResult.length) {\n logger.debug(\n `Unable to stitch ${entityRef}, item does not exist in refresh state table`,\n );\n return 'abandoned';\n }\n\n const {\n entityId,\n processedEntity,\n errors,\n incomingReferenceCount,\n previousHash,\n } = processedResult[0];\n\n // If there was no processed entity in place, the target hasn't been\n // through the processing steps yet. It's safe to ignore this stitch\n // attempt in that case, since another stitch will be triggered when\n // that processing has finished.\n if (!processedEntity) {\n logger.debug(\n `Unable to stitch ${entityRef}, the entity has not yet been processed`,\n );\n return 'abandoned';\n }\n\n // Grab the processed entity and stitch all of the relevant data into\n // it\n const entity = JSON.parse(processedEntity) as AlphaEntity;\n const isOrphan = Number(incomingReferenceCount) === 0;\n let statusItems: EntityStatusItem[] = [];\n\n if (isOrphan) {\n logger.debug(`${entityRef} is an orphan`);\n entity.metadata.annotations = {\n ...entity.metadata.annotations,\n ['backstage.io/orphan']: 'true',\n };\n }\n if (errors) {\n const parsedErrors = JSON.parse(errors) as SerializedError[];\n if (Array.isArray(parsedErrors) && parsedErrors.length) {\n statusItems = parsedErrors.map(e => ({\n type: ENTITY_STATUS_CATALOG_PROCESSING_TYPE,\n level: 'error',\n message: `${e.name}: ${e.message}`,\n error: e,\n }));\n }\n }\n // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors\n for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) {\n const value = entity.metadata.annotations?.[annotation];\n if (typeof value === 'string' && scriptProtocolPattern.test(value)) {\n entity.metadata.annotations![annotation] =\n 'https://backstage.io/annotation-rejected-for-security-reasons';\n }\n }\n\n // TODO: entityRef is lower case and should be uppercase in the final\n // result\n entity.relations = relationsResult\n .filter(row => row.relationType /* exclude null row, if relevant */)\n .map<EntityRelation>(row => ({\n type: row.relationType!,\n targetRef: row.relationTarget!,\n }));\n if (statusItems.length) {\n entity.status = {\n ...entity.status,\n items: [...(entity.status?.items ?? []), ...statusItems],\n };\n }\n\n // If the output entity was actually not changed, just abort\n const hash = generateStableHash(entity);\n if (hash === previousHash) {\n logger.debug(`Skipped stitching of ${entityRef}, no changes`);\n return 'unchanged';\n }\n\n entity.metadata.uid = entityId;\n if (!entity.metadata.etag) {\n // If the original data source did not have its own etag handling,\n // use the hash as a good-quality etag\n entity.metadata.etag = hash;\n }\n\n // This may throw if the entity is invalid, so we call it before\n // the final_entities write, even though we may end up not needing\n // to write the search index.\n const searchEntries = buildEntitySearch(entityId, entity);\n\n let updateQuery = knex<DbFinalEntitiesRow>('final_entities')\n .update({\n final_entity: JSON.stringify(entity),\n hash,\n last_updated_at: knex.fn.now(),\n })\n .where('entity_id', entityId);\n\n // In deferred mode, guard against concurrent stitchers by checking that\n // the stitch_ticket in stitch_queue still matches what we were given.\n if (options.strategy.mode === 'deferred' && stitchTicket) {\n updateQuery = updateQuery.whereExists(\n knex<DbStitchQueueRow>('stitch_queue')\n .where('stitch_queue.entity_ref', entityRef)\n .where('stitch_queue.stitch_ticket', stitchTicket)\n .select(knex.raw('1')),\n );\n }\n\n const amountOfRowsChanged = await updateQuery;\n\n if (amountOfRowsChanged === 0) {\n logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);\n return 'abandoned';\n }\n\n await knex.transaction(async trx => {\n await trx<DbSearchRow>('search').where({ entity_id: entityId }).delete();\n await trx.batchInsert('search', searchEntries, BATCH_SIZE);\n });\n\n return 'changed';\n } catch (error) {\n removeFromStitchQueueOnCompletion = false;\n throw error;\n } finally {\n if (removeFromStitchQueueOnCompletion && stitchTicket) {\n await markDeferredStitchCompleted({\n knex: knex,\n entityRef,\n stitchTicket,\n });\n }\n }\n}\n"],"names":["isDatabaseConflictError","ENTITY_STATUS_CATALOG_PROCESSING_TYPE","ANNOTATION_VIEW_URL","ANNOTATION_EDIT_URL","generateStableHash","buildEntitySearch","BATCH_SIZE","markDeferredStitchCompleted"],"mappings":"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"}
@@ -2,17 +2,17 @@
2
2
 
3
3
  var path = require('node:path');
4
4
  var minimatch = require('minimatch');
5
- var zod = require('zod');
5
+ var v3 = require('zod/v3');
6
6
 
7
7
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
8
 
9
9
  var path__default = /*#__PURE__*/_interopDefaultCompat(path);
10
10
 
11
- const allowRuleParser = zod.z.array(
12
- zod.z.object({
13
- kind: zod.z.string(),
14
- "spec.type": zod.z.string().optional()
15
- }).or(zod.z.string()).transform((val) => typeof val === "string" ? { kind: val } : val)
11
+ const allowRuleParser = v3.z.array(
12
+ v3.z.object({
13
+ kind: v3.z.string(),
14
+ "spec.type": v3.z.string().optional()
15
+ }).or(v3.z.string()).transform((val) => typeof val === "string" ? { kind: val } : val)
16
16
  );
17
17
  class DefaultCatalogRulesEnforcer {
18
18
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"CatalogRules.cjs.js","sources":["../../src/ingestion/CatalogRules.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { Entity } from '@backstage/catalog-model';\nimport path from 'node:path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\nimport { z } from 'zod';\n\n/**\n * Rules to apply to catalog entities.\n *\n * An undefined list of matchers means match all, an empty list of matchers means match none.\n */\nexport type CatalogRule = {\n allow: CatalogRuleAllow[];\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\n};\n\ntype CatalogRuleAllow = {\n kind: string;\n 'spec.type'?: string;\n};\n\n/**\n * Decides whether an entity from a given location is allowed to enter the\n * catalog, according to some rule set.\n */\nexport type CatalogRulesEnforcer = {\n isAllowed(entity: Entity, location: LocationSpec): boolean;\n};\n\nconst allowRuleParser = z.array(\n z\n .object({\n kind: z.string(),\n 'spec.type': z.string().optional(),\n })\n .or(z.string())\n .transform(val => (typeof val === 'string' ? { kind: val } : val)),\n);\n\n/**\n * Implements the default catalog rule set, consuming the config keys\n * `catalog.rules` and `catalog.locations.[].rules`.\n */\nexport class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {\n /**\n * Default rules used by the catalog.\n *\n * Denies any location from specifying user or group entities.\n */\n static readonly defaultRules: CatalogRule[] = [\n {\n allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),\n },\n ];\n\n /**\n * Loads catalog rules from config.\n *\n * This reads `catalog.rules` and defaults to the default rules if no value is present.\n * The value of the config should be a list of config objects, each with a single `allow`\n * field which in turn is a list of entity kinds to allow.\n *\n * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.\n *\n * It also reads in rules from `catalog.locations`, where each location can have a list\n * of rules for that specific location, specified in a `rules` field.\n *\n * For example:\n *\n * ```yaml\n * catalog:\n * rules:\n * - allow: [Component, API]\n * - allow:\n * - kind: Resource\n * 'spec.type': database\n * - allow: [Template]\n * locations:\n * - type: url\n * pattern: https://github.com/org/*\\/blob/master/template.yaml\n * - allow: [Location]\n * locations:\n * - type: url\n * pattern: https://github.com/org/repo/blob/master/location.yaml\n *\n * locations:\n * - type: url\n * target: https://github.com/org/repo/blob/master/users.yaml\n * rules:\n * - allow: [User, Group]\n * - type: url\n * target: https://github.com/org/repo/blob/master/systems.yaml\n * rules:\n * - allow: [System]\n * ```\n */\n static fromConfig(config: Config) {\n const rules = new Array<CatalogRule>();\n\n if (config.has('catalog.rules')) {\n const globalRules = config\n .getConfigArray('catalog.rules')\n .map(ruleConf => ({\n allow: allowRuleParser.parse(ruleConf.get('allow')),\n locations: ruleConf\n .getOptionalConfigArray('locations')\n ?.map(locationConfig => {\n const location = {\n pattern: locationConfig.getOptionalString('pattern'),\n type: locationConfig.getString('type'),\n exact: locationConfig.getOptionalString('exact'),\n };\n if (location.pattern && location.exact) {\n throw new Error(\n 'A catalog rule location cannot have both exact and pattern values',\n );\n }\n return location;\n }),\n }));\n rules.push(...globalRules);\n } else {\n rules.push(...DefaultCatalogRulesEnforcer.defaultRules);\n }\n\n if (config.has('catalog.locations')) {\n const locationRules = config\n .getConfigArray('catalog.locations')\n .flatMap(locConf => {\n if (!locConf.has('rules')) {\n return [];\n }\n const type = locConf.getString('type');\n const exact = resolveTarget(type, locConf.getString('target'));\n\n return locConf.getConfigArray('rules').map(ruleConf => ({\n allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),\n locations: [{ type, exact }],\n }));\n });\n\n rules.push(...locationRules);\n }\n\n return new DefaultCatalogRulesEnforcer(rules);\n }\n\n private readonly rules: CatalogRule[];\n\n constructor(rules: CatalogRule[]) {\n this.rules = rules;\n }\n\n /**\n * Checks whether a specific entity/location combination is allowed\n * according to the configured rules.\n */\n isAllowed(entity: Entity, location: LocationSpec) {\n for (const rule of this.rules) {\n if (!this.matchLocation(location, rule.locations)) {\n continue;\n }\n\n if (this.matchEntity(entity, rule.allow)) {\n return true;\n }\n }\n\n return false;\n }\n\n private matchLocation(\n location: LocationSpec,\n matchers?: { exact?: string; type: string; pattern?: string }[],\n ): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (matcher.type !== location?.type) {\n continue;\n }\n if (matcher.exact && matcher.exact !== location?.target) {\n continue;\n }\n if (\n matcher.pattern &&\n !minimatch(location?.target, matcher.pattern, {\n nocase: true,\n dot: true,\n })\n ) {\n continue;\n }\n return true;\n }\n\n return false;\n }\n\n private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (\n entity.kind?.toLocaleLowerCase('en-US') !==\n matcher.kind.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n\n if (matcher['spec.type']) {\n if (typeof entity.spec?.type !== 'string') {\n continue;\n }\n if (\n matcher['spec.type'].toLocaleLowerCase('en-US') !==\n entity.spec.type.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n }\n\n return true;\n }\n\n return false;\n }\n}\n\nfunction resolveTarget(type: string, target: string): string {\n if (type !== 'file') {\n return target;\n }\n\n return path.resolve(target);\n}\n"],"names":["z","minimatch","path"],"mappings":"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"}
1
+ {"version":3,"file":"CatalogRules.cjs.js","sources":["../../src/ingestion/CatalogRules.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { Config } from '@backstage/config';\nimport { Entity } from '@backstage/catalog-model';\nimport path from 'node:path';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport { minimatch } from 'minimatch';\nimport { z } from 'zod/v3';\n\n/**\n * Rules to apply to catalog entities.\n *\n * An undefined list of matchers means match all, an empty list of matchers means match none.\n */\nexport type CatalogRule = {\n allow: CatalogRuleAllow[];\n locations?: Array<{\n exact?: string;\n type: string;\n pattern?: string;\n }>;\n};\n\ntype CatalogRuleAllow = {\n kind: string;\n 'spec.type'?: string;\n};\n\n/**\n * Decides whether an entity from a given location is allowed to enter the\n * catalog, according to some rule set.\n */\nexport type CatalogRulesEnforcer = {\n isAllowed(entity: Entity, location: LocationSpec): boolean;\n};\n\nconst allowRuleParser = z.array(\n z\n .object({\n kind: z.string(),\n 'spec.type': z.string().optional(),\n })\n .or(z.string())\n .transform(val => (typeof val === 'string' ? { kind: val } : val)),\n);\n\n/**\n * Implements the default catalog rule set, consuming the config keys\n * `catalog.rules` and `catalog.locations.[].rules`.\n */\nexport class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer {\n /**\n * Default rules used by the catalog.\n *\n * Denies any location from specifying user or group entities.\n */\n static readonly defaultRules: CatalogRule[] = [\n {\n allow: ['Component', 'API', 'Location'].map(kind => ({ kind })),\n },\n ];\n\n /**\n * Loads catalog rules from config.\n *\n * This reads `catalog.rules` and defaults to the default rules if no value is present.\n * The value of the config should be a list of config objects, each with a single `allow`\n * field which in turn is a list of entity kinds to allow.\n *\n * If there is no matching rule to allow an ingested entity, it will be rejected by the catalog.\n *\n * It also reads in rules from `catalog.locations`, where each location can have a list\n * of rules for that specific location, specified in a `rules` field.\n *\n * For example:\n *\n * ```yaml\n * catalog:\n * rules:\n * - allow: [Component, API]\n * - allow:\n * - kind: Resource\n * 'spec.type': database\n * - allow: [Template]\n * locations:\n * - type: url\n * pattern: https://github.com/org/*\\/blob/master/template.yaml\n * - allow: [Location]\n * locations:\n * - type: url\n * pattern: https://github.com/org/repo/blob/master/location.yaml\n *\n * locations:\n * - type: url\n * target: https://github.com/org/repo/blob/master/users.yaml\n * rules:\n * - allow: [User, Group]\n * - type: url\n * target: https://github.com/org/repo/blob/master/systems.yaml\n * rules:\n * - allow: [System]\n * ```\n */\n static fromConfig(config: Config) {\n const rules = new Array<CatalogRule>();\n\n if (config.has('catalog.rules')) {\n const globalRules = config\n .getConfigArray('catalog.rules')\n .map(ruleConf => ({\n allow: allowRuleParser.parse(ruleConf.get('allow')),\n locations: ruleConf\n .getOptionalConfigArray('locations')\n ?.map(locationConfig => {\n const location = {\n pattern: locationConfig.getOptionalString('pattern'),\n type: locationConfig.getString('type'),\n exact: locationConfig.getOptionalString('exact'),\n };\n if (location.pattern && location.exact) {\n throw new Error(\n 'A catalog rule location cannot have both exact and pattern values',\n );\n }\n return location;\n }),\n }));\n rules.push(...globalRules);\n } else {\n rules.push(...DefaultCatalogRulesEnforcer.defaultRules);\n }\n\n if (config.has('catalog.locations')) {\n const locationRules = config\n .getConfigArray('catalog.locations')\n .flatMap(locConf => {\n if (!locConf.has('rules')) {\n return [];\n }\n const type = locConf.getString('type');\n const exact = resolveTarget(type, locConf.getString('target'));\n\n return locConf.getConfigArray('rules').map(ruleConf => ({\n allow: ruleConf.getStringArray('allow').map(kind => ({ kind })),\n locations: [{ type, exact }],\n }));\n });\n\n rules.push(...locationRules);\n }\n\n return new DefaultCatalogRulesEnforcer(rules);\n }\n\n private readonly rules: CatalogRule[];\n\n constructor(rules: CatalogRule[]) {\n this.rules = rules;\n }\n\n /**\n * Checks whether a specific entity/location combination is allowed\n * according to the configured rules.\n */\n isAllowed(entity: Entity, location: LocationSpec) {\n for (const rule of this.rules) {\n if (!this.matchLocation(location, rule.locations)) {\n continue;\n }\n\n if (this.matchEntity(entity, rule.allow)) {\n return true;\n }\n }\n\n return false;\n }\n\n private matchLocation(\n location: LocationSpec,\n matchers?: { exact?: string; type: string; pattern?: string }[],\n ): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (matcher.type !== location?.type) {\n continue;\n }\n if (matcher.exact && matcher.exact !== location?.target) {\n continue;\n }\n if (\n matcher.pattern &&\n !minimatch(location?.target, matcher.pattern, {\n nocase: true,\n dot: true,\n })\n ) {\n continue;\n }\n return true;\n }\n\n return false;\n }\n\n private matchEntity(entity: Entity, matchers?: CatalogRuleAllow[]): boolean {\n if (!matchers) {\n return true;\n }\n\n for (const matcher of matchers) {\n if (\n entity.kind?.toLocaleLowerCase('en-US') !==\n matcher.kind.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n\n if (matcher['spec.type']) {\n if (typeof entity.spec?.type !== 'string') {\n continue;\n }\n if (\n matcher['spec.type'].toLocaleLowerCase('en-US') !==\n entity.spec.type.toLocaleLowerCase('en-US')\n ) {\n continue;\n }\n }\n\n return true;\n }\n\n return false;\n }\n}\n\nfunction resolveTarget(type: string, target: string): string {\n if (type !== 'file') {\n return target;\n }\n\n return path.resolve(target);\n}\n"],"names":["z","minimatch","path"],"mappings":"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"}
@@ -3,15 +3,15 @@
3
3
  var alpha = require('@backstage/plugin-catalog-node/alpha');
4
4
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
5
5
  var lodash = require('lodash');
6
- var zod = require('zod');
6
+ var v3 = require('zod/v3');
7
7
 
8
8
  const createPropertyRule = (propertyType) => pluginPermissionNode.createPermissionRule({
9
9
  name: `HAS_${propertyType.toUpperCase()}`,
10
10
  description: `Allow entities with the specified ${propertyType} subfield`,
11
11
  resourceRef: alpha.catalogEntityPermissionResourceRef,
12
- paramsSchema: zod.z.object({
13
- key: zod.z.string().describe(`Property within the entities ${propertyType} to match on`),
14
- value: zod.z.string().optional().describe(`Value of the given property to match on`)
12
+ paramsSchema: v3.z.object({
13
+ key: v3.z.string().describe(`Property within the entities ${propertyType} to match on`),
14
+ value: v3.z.string().optional().describe(`Value of the given property to match on`)
15
15
  }),
16
16
  apply: (resource, { key, value }) => {
17
17
  const foundValue = lodash.get(resource[propertyType], key);
@@ -1 +1 @@
1
- {"version":3,"file":"createPropertyRule.cjs.js","sources":["../../../src/permissions/rules/createPropertyRule.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { get } from 'lodash';\nimport { z } from 'zod';\n\nexport const createPropertyRule = (propertyType: 'metadata' | 'spec') =>\n createPermissionRule({\n name: `HAS_${propertyType.toUpperCase()}`,\n description: `Allow entities with the specified ${propertyType} subfield`,\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n key: z\n .string()\n .describe(`Property within the entities ${propertyType} to match on`),\n value: z\n .string()\n .optional()\n .describe(`Value of the given property to match on`),\n }),\n apply: (resource, { key, value }) => {\n const foundValue = get(resource[propertyType], key);\n\n if (Array.isArray(foundValue)) {\n if (value !== undefined) {\n return foundValue.includes(value);\n }\n return foundValue.length > 0;\n }\n if (value !== undefined) {\n return value === foundValue;\n }\n return !!foundValue;\n },\n toQuery: ({ key, value }) => ({\n key: `${propertyType}.${key}`,\n ...(value !== undefined && { values: [value] }),\n }),\n });\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z","get"],"mappings":";;;;;;;AAqBO,MAAM,kBAAA,GAAqB,CAAC,YAAA,KACjCA,yCAAA,CAAqB;AAAA,EACnB,IAAA,EAAM,CAAA,IAAA,EAAO,YAAA,CAAa,WAAA,EAAa,CAAA,CAAA;AAAA,EACvC,WAAA,EAAa,qCAAqC,YAAY,CAAA,SAAA,CAAA;AAAA,EAC9D,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,MAAE,MAAA,CAAO;AAAA,IACrB,KAAKA,KAAA,CACF,MAAA,GACA,QAAA,CAAS,CAAA,6BAAA,EAAgC,YAAY,CAAA,YAAA,CAAc,CAAA;AAAA,IACtE,OAAOA,KAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,CAAA,uCAAA,CAAyC;AAAA,GACtD,CAAA;AAAA,EACD,OAAO,CAAC,QAAA,EAAU,EAAE,GAAA,EAAK,OAAM,KAAM;AACnC,IAAA,MAAM,UAAA,GAAaC,UAAA,CAAI,QAAA,CAAS,YAAY,GAAG,GAAG,CAAA;AAElD,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC7B,MAAA,IAAI,UAAU,MAAA,EAAW;AACvB,QAAA,OAAO,UAAA,CAAW,SAAS,KAAK,CAAA;AAAA,MAClC;AACA,MAAA,OAAO,WAAW,MAAA,GAAS,CAAA;AAAA,IAC7B;AACA,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,OAAO,KAAA,KAAU,UAAA;AAAA,IACnB;AACA,IAAA,OAAO,CAAC,CAAC,UAAA;AAAA,EACX,CAAA;AAAA,EACA,OAAA,EAAS,CAAC,EAAE,GAAA,EAAK,OAAM,MAAO;AAAA,IAC5B,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAAA,IAC3B,GAAI,KAAA,KAAU,MAAA,IAAa,EAAE,MAAA,EAAQ,CAAC,KAAK,CAAA;AAAE,GAC/C;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"createPropertyRule.cjs.js","sources":["../../../src/permissions/rules/createPropertyRule.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { get } from 'lodash';\nimport { z } from 'zod/v3';\n\nexport const createPropertyRule = (propertyType: 'metadata' | 'spec') =>\n createPermissionRule({\n name: `HAS_${propertyType.toUpperCase()}`,\n description: `Allow entities with the specified ${propertyType} subfield`,\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n key: z\n .string()\n .describe(`Property within the entities ${propertyType} to match on`),\n value: z\n .string()\n .optional()\n .describe(`Value of the given property to match on`),\n }),\n apply: (resource, { key, value }) => {\n const foundValue = get(resource[propertyType], key);\n\n if (Array.isArray(foundValue)) {\n if (value !== undefined) {\n return foundValue.includes(value);\n }\n return foundValue.length > 0;\n }\n if (value !== undefined) {\n return value === foundValue;\n }\n return !!foundValue;\n },\n toQuery: ({ key, value }) => ({\n key: `${propertyType}.${key}`,\n ...(value !== undefined && { values: [value] }),\n }),\n });\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z","get"],"mappings":";;;;;;;AAqBO,MAAM,kBAAA,GAAqB,CAAC,YAAA,KACjCA,yCAAA,CAAqB;AAAA,EACnB,IAAA,EAAM,CAAA,IAAA,EAAO,YAAA,CAAa,WAAA,EAAa,CAAA,CAAA;AAAA,EACvC,WAAA,EAAa,qCAAqC,YAAY,CAAA,SAAA,CAAA;AAAA,EAC9D,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,KAAKA,IAAA,CACF,MAAA,GACA,QAAA,CAAS,CAAA,6BAAA,EAAgC,YAAY,CAAA,YAAA,CAAc,CAAA;AAAA,IACtE,OAAOA,IAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,CAAA,uCAAA,CAAyC;AAAA,GACtD,CAAA;AAAA,EACD,OAAO,CAAC,QAAA,EAAU,EAAE,GAAA,EAAK,OAAM,KAAM;AACnC,IAAA,MAAM,UAAA,GAAaC,UAAA,CAAI,QAAA,CAAS,YAAY,GAAG,GAAG,CAAA;AAElD,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC7B,MAAA,IAAI,UAAU,MAAA,EAAW;AACvB,QAAA,OAAO,UAAA,CAAW,SAAS,KAAK,CAAA;AAAA,MAClC;AACA,MAAA,OAAO,WAAW,MAAA,GAAS,CAAA;AAAA,IAC7B;AACA,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,OAAO,KAAA,KAAU,UAAA;AAAA,IACnB;AACA,IAAA,OAAO,CAAC,CAAC,UAAA;AAAA,EACX,CAAA;AAAA,EACA,OAAA,EAAS,CAAC,EAAE,GAAA,EAAK,OAAM,MAAO;AAAA,IAC5B,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAAA,IAC3B,GAAI,KAAA,KAAU,MAAA,IAAa,EAAE,MAAA,EAAQ,CAAC,KAAK,CAAA;AAAE,GAC/C;AACF,CAAC;;;;"}
@@ -2,15 +2,15 @@
2
2
 
3
3
  var alpha = require('@backstage/plugin-catalog-node/alpha');
4
4
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
5
- var zod = require('zod');
5
+ var v3 = require('zod/v3');
6
6
 
7
7
  const hasAnnotation = pluginPermissionNode.createPermissionRule({
8
8
  name: "HAS_ANNOTATION",
9
9
  description: "Allow entities with the specified annotation",
10
10
  resourceRef: alpha.catalogEntityPermissionResourceRef,
11
- paramsSchema: zod.z.object({
12
- annotation: zod.z.string().describe("Name of the annotation to match on"),
13
- value: zod.z.string().optional().describe("Value of the annotation to match on")
11
+ paramsSchema: v3.z.object({
12
+ annotation: v3.z.string().describe("Name of the annotation to match on"),
13
+ value: v3.z.string().optional().describe("Value of the annotation to match on")
14
14
  }),
15
15
  apply: (resource, { annotation, value }) => !!resource.metadata.annotations?.hasOwnProperty(annotation) && (value === void 0 ? true : resource.metadata.annotations?.[annotation] === value),
16
16
  toQuery: ({ annotation, value }) => value === void 0 ? {
@@ -1 +1 @@
1
- {"version":3,"file":"hasAnnotation.cjs.js","sources":["../../../src/permissions/rules/hasAnnotation.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for the presence of an annotation on a given entity.\n *\n * If a value is given, it filters for the annotation value, too.\n *\n * @alpha\n */\nexport const hasAnnotation = createPermissionRule({\n name: 'HAS_ANNOTATION',\n description: 'Allow entities with the specified annotation',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n annotation: z.string().describe('Name of the annotation to match on'),\n value: z\n .string()\n .optional()\n .describe('Value of the annotation to match on'),\n }),\n apply: (resource, { annotation, value }) =>\n !!resource.metadata.annotations?.hasOwnProperty(annotation) &&\n (value === undefined\n ? true\n : resource.metadata.annotations?.[annotation] === value),\n toQuery: ({ annotation, value }) =>\n value === undefined\n ? {\n key: `metadata.annotations.${annotation}`,\n }\n : {\n key: `metadata.annotations.${annotation}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AA4BO,MAAM,gBAAgBA,yCAAA,CAAqB;AAAA,EAChD,IAAA,EAAM,gBAAA;AAAA,EACN,WAAA,EAAa,8CAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,MAAE,MAAA,CAAO;AAAA,IACrB,UAAA,EAAYA,KAAA,CAAE,MAAA,EAAO,CAAE,SAAS,oCAAoC,CAAA;AAAA,IACpE,OAAOA,KAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,qCAAqC;AAAA,GAClD,CAAA;AAAA,EACD,KAAA,EAAO,CAAC,QAAA,EAAU,EAAE,YAAY,KAAA,EAAM,KACpC,CAAC,CAAC,QAAA,CAAS,QAAA,CAAS,aAAa,cAAA,CAAe,UAAU,MACzD,KAAA,KAAU,MAAA,GACP,OACA,QAAA,CAAS,QAAA,CAAS,WAAA,GAAc,UAAU,CAAA,KAAM,KAAA,CAAA;AAAA,EACtD,SAAS,CAAC,EAAE,YAAY,KAAA,EAAM,KAC5B,UAAU,MAAA,GACN;AAAA,IACE,GAAA,EAAK,wBAAwB,UAAU,CAAA;AAAA,GACzC,GACA;AAAA,IACE,GAAA,EAAK,wBAAwB,UAAU,CAAA,CAAA;AAAA,IACvC,MAAA,EAAQ,CAAC,KAAK;AAAA;AAExB,CAAC;;;;"}
1
+ {"version":3,"file":"hasAnnotation.cjs.js","sources":["../../../src/permissions/rules/hasAnnotation.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod/v3';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for the presence of an annotation on a given entity.\n *\n * If a value is given, it filters for the annotation value, too.\n *\n * @alpha\n */\nexport const hasAnnotation = createPermissionRule({\n name: 'HAS_ANNOTATION',\n description: 'Allow entities with the specified annotation',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n annotation: z.string().describe('Name of the annotation to match on'),\n value: z\n .string()\n .optional()\n .describe('Value of the annotation to match on'),\n }),\n apply: (resource, { annotation, value }) =>\n !!resource.metadata.annotations?.hasOwnProperty(annotation) &&\n (value === undefined\n ? true\n : resource.metadata.annotations?.[annotation] === value),\n toQuery: ({ annotation, value }) =>\n value === undefined\n ? {\n key: `metadata.annotations.${annotation}`,\n }\n : {\n key: `metadata.annotations.${annotation}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AA4BO,MAAM,gBAAgBA,yCAAA,CAAqB;AAAA,EAChD,IAAA,EAAM,gBAAA;AAAA,EACN,WAAA,EAAa,8CAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,UAAA,EAAYA,IAAA,CAAE,MAAA,EAAO,CAAE,SAAS,oCAAoC,CAAA;AAAA,IACpE,OAAOA,IAAA,CACJ,MAAA,GACA,QAAA,EAAS,CACT,SAAS,qCAAqC;AAAA,GAClD,CAAA;AAAA,EACD,KAAA,EAAO,CAAC,QAAA,EAAU,EAAE,YAAY,KAAA,EAAM,KACpC,CAAC,CAAC,QAAA,CAAS,QAAA,CAAS,aAAa,cAAA,CAAe,UAAU,MACzD,KAAA,KAAU,MAAA,GACP,OACA,QAAA,CAAS,QAAA,CAAS,WAAA,GAAc,UAAU,CAAA,KAAM,KAAA,CAAA;AAAA,EACtD,SAAS,CAAC,EAAE,YAAY,KAAA,EAAM,KAC5B,UAAU,MAAA,GACN;AAAA,IACE,GAAA,EAAK,wBAAwB,UAAU,CAAA;AAAA,GACzC,GACA;AAAA,IACE,GAAA,EAAK,wBAAwB,UAAU,CAAA,CAAA;AAAA,IACvC,MAAA,EAAQ,CAAC,KAAK;AAAA;AAExB,CAAC;;;;"}
@@ -2,15 +2,15 @@
2
2
 
3
3
  var alpha = require('@backstage/plugin-catalog-node/alpha');
4
4
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
5
- var zod = require('zod');
5
+ var v3 = require('zod/v3');
6
6
 
7
7
  const hasLabel = pluginPermissionNode.createPermissionRule({
8
8
  name: "HAS_LABEL",
9
9
  description: "Allow entities with the specified label",
10
10
  resourceRef: alpha.catalogEntityPermissionResourceRef,
11
- paramsSchema: zod.z.object({
12
- label: zod.z.string().describe("Name of the label to match on"),
13
- value: zod.z.string().optional().describe("Value of the label to match on")
11
+ paramsSchema: v3.z.object({
12
+ label: v3.z.string().describe("Name of the label to match on"),
13
+ value: v3.z.string().optional().describe("Value of the label to match on")
14
14
  }),
15
15
  apply: (resource, { label, value }) => !!resource.metadata.labels?.hasOwnProperty(label) && (value === void 0 ? true : resource.metadata.labels?.[label] === value),
16
16
  toQuery: ({ label, value }) => value === void 0 ? {
@@ -1 +1 @@
1
- {"version":3,"file":"hasLabel.cjs.js","sources":["../../../src/permissions/rules/hasLabel.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for entities with a specified label in its metadata.\n * @alpha\n */\nexport const hasLabel = createPermissionRule({\n name: 'HAS_LABEL',\n description: 'Allow entities with the specified label',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n label: z.string().describe('Name of the label to match on'),\n value: z.string().optional().describe('Value of the label to match on'),\n }),\n apply: (resource, { label, value }) =>\n !!resource.metadata.labels?.hasOwnProperty(label) &&\n (value === undefined ? true : resource.metadata.labels?.[label] === value),\n toQuery: ({ label, value }) =>\n value === undefined\n ? {\n key: `metadata.labels.${label}`,\n }\n : {\n key: `metadata.labels.${label}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AAyBO,MAAM,WAAWA,yCAAA,CAAqB;AAAA,EAC3C,IAAA,EAAM,WAAA;AAAA,EACN,WAAA,EAAa,yCAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,MAAE,MAAA,CAAO;AAAA,IACrB,KAAA,EAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,SAAS,+BAA+B,CAAA;AAAA,IAC1D,OAAOA,KAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,gCAAgC;AAAA,GACvE,CAAA;AAAA,EACD,KAAA,EAAO,CAAC,QAAA,EAAU,EAAE,OAAO,KAAA,EAAM,KAC/B,CAAC,CAAC,QAAA,CAAS,QAAA,CAAS,QAAQ,cAAA,CAAe,KAAK,MAC/C,KAAA,KAAU,MAAA,GAAY,OAAO,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,KAAK,CAAA,KAAM,KAAA,CAAA;AAAA,EACtE,SAAS,CAAC,EAAE,OAAO,KAAA,EAAM,KACvB,UAAU,MAAA,GACN;AAAA,IACE,GAAA,EAAK,mBAAmB,KAAK,CAAA;AAAA,GAC/B,GACA;AAAA,IACE,GAAA,EAAK,mBAAmB,KAAK,CAAA,CAAA;AAAA,IAC7B,MAAA,EAAQ,CAAC,KAAK;AAAA;AAExB,CAAC;;;;"}
1
+ {"version":3,"file":"hasLabel.cjs.js","sources":["../../../src/permissions/rules/hasLabel.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 { catalogEntityPermissionResourceRef } from '@backstage/plugin-catalog-node/alpha';\nimport { createPermissionRule } from '@backstage/plugin-permission-node';\nimport { z } from 'zod/v3';\n\n/**\n * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which\n * filters for entities with a specified label in its metadata.\n * @alpha\n */\nexport const hasLabel = createPermissionRule({\n name: 'HAS_LABEL',\n description: 'Allow entities with the specified label',\n resourceRef: catalogEntityPermissionResourceRef,\n paramsSchema: z.object({\n label: z.string().describe('Name of the label to match on'),\n value: z.string().optional().describe('Value of the label to match on'),\n }),\n apply: (resource, { label, value }) =>\n !!resource.metadata.labels?.hasOwnProperty(label) &&\n (value === undefined ? true : resource.metadata.labels?.[label] === value),\n toQuery: ({ label, value }) =>\n value === undefined\n ? {\n key: `metadata.labels.${label}`,\n }\n : {\n key: `metadata.labels.${label}`,\n values: [value],\n },\n});\n"],"names":["createPermissionRule","catalogEntityPermissionResourceRef","z"],"mappings":";;;;;;AAyBO,MAAM,WAAWA,yCAAA,CAAqB;AAAA,EAC3C,IAAA,EAAM,WAAA;AAAA,EACN,WAAA,EAAa,yCAAA;AAAA,EACb,WAAA,EAAaC,wCAAA;AAAA,EACb,YAAA,EAAcC,KAAE,MAAA,CAAO;AAAA,IACrB,KAAA,EAAOA,IAAA,CAAE,MAAA,EAAO,CAAE,SAAS,+BAA+B,CAAA;AAAA,IAC1D,OAAOA,IAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,gCAAgC;AAAA,GACvE,CAAA;AAAA,EACD,KAAA,EAAO,CAAC,QAAA,EAAU,EAAE,OAAO,KAAA,EAAM,KAC/B,CAAC,CAAC,QAAA,CAAS,QAAA,CAAS,QAAQ,cAAA,CAAe,KAAK,MAC/C,KAAA,KAAU,MAAA,GAAY,OAAO,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,KAAK,CAAA,KAAM,KAAA,CAAA;AAAA,EACtE,SAAS,CAAC,EAAE,OAAO,KAAA,EAAM,KACvB,UAAU,MAAA,GACN;AAAA,IACE,GAAA,EAAK,mBAAmB,KAAK,CAAA;AAAA,GAC/B,GACA;AAAA,IACE,GAAA,EAAK,mBAAmB,KAAK,CAAA,CAAA;AAAA,IAC7B,MAAA,EAAQ,CAAC,KAAK;AAAA;AAExB,CAAC;;;;"}