@thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor 1.0.2

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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor
2
+
3
+ A backend module for the Backstage catalog that processes `spec.owners` on entities and emits proper `ownedBy` / `ownerOf` relations for each owner.
4
+
5
+ ![screenshow](https://github.com/TheCodingSheikh/backstage-plugins/blob/main/plugins/multi-owner/catalog-backend-module-multi-owner-processor/screenshot.png?raw=true)
6
+
7
+ ## Features
8
+
9
+ - Reads `spec.owners` (an array of strings or `{ name, role }` objects) from any entity
10
+ - Emits bidirectional `ownedBy` / `ownerOf` relations for each owner
11
+ - Writes a normalized `multi-owner.io/owners` annotation (JSON) for the frontend to consume
12
+ - Coexists with the built-in `spec.owner` field — both are merged automatically
13
+ - Defaults unqualified references to `kind: Group`
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ yarn --cwd packages/backend @thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor
19
+ ```
20
+
21
+ ### Backend Setup
22
+
23
+ In your `packages/backend/src/index.ts`:
24
+
25
+ ```ts
26
+ const backend = createBackend();
27
+
28
+ // ... other plugins ...
29
+
30
+ // Multi-owner processor
31
+ backend.add(
32
+ import(
33
+ '@thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor'
34
+ ),
35
+ );
36
+
37
+ backend.start();
38
+ ```
39
+
40
+ ## Entity Configuration
41
+
42
+ Add `spec.owners` to any entity's `catalog-info.yaml`:
43
+
44
+ ```yaml
45
+ apiVersion: backstage.io/v1alpha1
46
+ kind: Component
47
+ metadata:
48
+ name: my-service
49
+ spec:
50
+ type: service
51
+ lifecycle: production
52
+ owners:
53
+ - name: group:default/platform-team
54
+ role: maintainer
55
+ - name: group:default/sre-team
56
+ role: operations
57
+ - name: user:default/jane
58
+ role: tech-lead
59
+ - group:default/qa-team # string shorthand, no role
60
+ ```
61
+
62
+ ## How It Works
63
+
64
+ ```mermaid
65
+ graph LR
66
+ A[Entity YAML] -->|spec.owners| B[MultiOwnerEntitiesProcessor]
67
+ B -->|preProcess| C[Writes annotation]
68
+ B -->|postProcess| D[Emits ownedBy relations]
69
+ B -->|postProcess| E[Emits ownerOf relations]
70
+ C --> F[Frontend Card reads annotation]
71
+ D --> G[Backstage ownership features]
72
+ ```
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
4
+ var catalogModel = require('@backstage/catalog-model');
5
+ var backstagePluginMultiOwnerCommon = require('@thecodingsheikh/backstage-plugin-multi-owner-common');
6
+
7
+ class MultiOwnerEntitiesProcessor {
8
+ getProcessorName() {
9
+ return "MultiOwnerEntitiesProcessor";
10
+ }
11
+ async preProcessEntity(entity, _location) {
12
+ const spec = entity.spec;
13
+ if (!spec?.owners) {
14
+ return entity;
15
+ }
16
+ const owners = backstagePluginMultiOwnerCommon.parseOwners(spec.owners);
17
+ if (owners.length === 0) {
18
+ return entity;
19
+ }
20
+ return {
21
+ ...entity,
22
+ metadata: {
23
+ ...entity.metadata,
24
+ annotations: {
25
+ ...entity.metadata.annotations,
26
+ [backstagePluginMultiOwnerCommon.MULTI_OWNER_ANNOTATION]: JSON.stringify(owners)
27
+ }
28
+ }
29
+ };
30
+ }
31
+ async postProcessEntity(entity, _location, emit) {
32
+ const spec = entity.spec;
33
+ if (!spec?.owners) {
34
+ return entity;
35
+ }
36
+ const owners = backstagePluginMultiOwnerCommon.parseOwners(spec.owners);
37
+ for (const owner of owners) {
38
+ let ownerRef;
39
+ try {
40
+ const parsed = catalogModel.parseEntityRef(owner.name, {
41
+ defaultKind: "group",
42
+ defaultNamespace: entity.metadata.namespace || "default"
43
+ });
44
+ ownerRef = catalogModel.stringifyEntityRef(parsed);
45
+ } catch {
46
+ continue;
47
+ }
48
+ emit(
49
+ pluginCatalogNode.processingResult.relation({
50
+ type: catalogModel.RELATION_OWNED_BY,
51
+ source: {
52
+ kind: entity.kind,
53
+ namespace: entity.metadata.namespace || "default",
54
+ name: entity.metadata.name
55
+ },
56
+ target: catalogModel.parseEntityRef(ownerRef)
57
+ })
58
+ );
59
+ emit(
60
+ pluginCatalogNode.processingResult.relation({
61
+ type: catalogModel.RELATION_OWNER_OF,
62
+ source: catalogModel.parseEntityRef(ownerRef),
63
+ target: {
64
+ kind: entity.kind,
65
+ namespace: entity.metadata.namespace || "default",
66
+ name: entity.metadata.name
67
+ }
68
+ })
69
+ );
70
+ }
71
+ return entity;
72
+ }
73
+ }
74
+
75
+ exports.MultiOwnerEntitiesProcessor = MultiOwnerEntitiesProcessor;
76
+ //# sourceMappingURL=MultiOwnerEntitiesProcessor.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiOwnerEntitiesProcessor.cjs.js","sources":["../src/MultiOwnerEntitiesProcessor.ts"],"sourcesContent":["import {\n CatalogProcessor,\n CatalogProcessorEmit,\n processingResult,\n} from '@backstage/plugin-catalog-node';\nimport { Entity } from '@backstage/catalog-model';\nimport { LocationSpec } from '@backstage/plugin-catalog-common';\nimport {\n RELATION_OWNED_BY,\n RELATION_OWNER_OF,\n parseEntityRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport {\n MULTI_OWNER_ANNOTATION,\n parseOwners,\n} from '@thecodingsheikh/backstage-plugin-multi-owner-common';\n\n/**\n * A catalog processor that reads `spec.owners` from entities and emits\n * `ownedBy` / `ownerOf` relations for each owner listed.\n *\n * It also writes the normalized owner list as a JSON annotation\n * (`backstage.io/owners`) so the frontend can display them.\n *\n * @remarks\n * This processor runs *in addition to* the built-in processor that handles\n * `spec.owner`. If both fields are present, owners from both are emitted.\n * Duplicate relations are automatically deduplicated by the catalog engine.\n */\nexport class MultiOwnerEntitiesProcessor implements CatalogProcessor {\n getProcessorName(): string {\n return 'MultiOwnerEntitiesProcessor';\n }\n\n async preProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n ): Promise<Entity> {\n const spec = entity.spec as Record<string, unknown> | undefined;\n if (!spec?.owners) {\n return entity;\n }\n\n const owners = parseOwners(spec.owners);\n if (owners.length === 0) {\n return entity;\n }\n\n // Write the normalized owner list as a JSON annotation so the\n // frontend card can read it without re-parsing spec.\n return {\n ...entity,\n metadata: {\n ...entity.metadata,\n annotations: {\n ...entity.metadata.annotations,\n [MULTI_OWNER_ANNOTATION]: JSON.stringify(owners),\n },\n },\n };\n }\n\n async postProcessEntity(\n entity: Entity,\n _location: LocationSpec,\n emit: CatalogProcessorEmit,\n ): Promise<Entity> {\n const spec = entity.spec as Record<string, unknown> | undefined;\n if (!spec?.owners) {\n return entity;\n }\n\n const owners = parseOwners(spec.owners);\n\n for (const owner of owners) {\n let ownerRef: string;\n try {\n // Validate and normalize the entity reference\n const parsed = parseEntityRef(owner.name, {\n defaultKind: 'group',\n defaultNamespace: entity.metadata.namespace || 'default',\n });\n ownerRef = stringifyEntityRef(parsed);\n } catch {\n // Skip invalid references\n continue;\n }\n\n // Emit the bidirectional ownership relations\n emit(\n processingResult.relation({\n type: RELATION_OWNED_BY,\n source: {\n kind: entity.kind,\n namespace: entity.metadata.namespace || 'default',\n name: entity.metadata.name,\n },\n target: parseEntityRef(ownerRef),\n }),\n );\n\n emit(\n processingResult.relation({\n type: RELATION_OWNER_OF,\n source: parseEntityRef(ownerRef),\n target: {\n kind: entity.kind,\n namespace: entity.metadata.namespace || 'default',\n name: entity.metadata.name,\n },\n }),\n );\n }\n\n return entity;\n }\n}\n"],"names":["parseOwners","MULTI_OWNER_ANNOTATION","parseEntityRef","stringifyEntityRef","processingResult","RELATION_OWNED_BY","RELATION_OWNER_OF"],"mappings":";;;;;;AA8BO,MAAM,2BAAA,CAAwD;AAAA,EACjE,gBAAA,GAA2B;AACvB,IAAA,OAAO,6BAAA;AAAA,EACX;AAAA,EAEA,MAAM,gBAAA,CACF,MAAA,EACA,SAAA,EACe;AACf,IAAA,MAAM,OAAO,MAAA,CAAO,IAAA;AACpB,IAAA,IAAI,CAAC,MAAM,MAAA,EAAQ;AACf,MAAA,OAAO,MAAA;AAAA,IACX;AAEA,IAAA,MAAM,MAAA,GAASA,2CAAA,CAAY,IAAA,CAAK,MAAM,CAAA;AACtC,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACrB,MAAA,OAAO,MAAA;AAAA,IACX;AAIA,IAAA,OAAO;AAAA,MACH,GAAG,MAAA;AAAA,MACH,QAAA,EAAU;AAAA,QACN,GAAG,MAAA,CAAO,QAAA;AAAA,QACV,WAAA,EAAa;AAAA,UACT,GAAG,OAAO,QAAA,CAAS,WAAA;AAAA,UACnB,CAACC,sDAAsB,GAAG,IAAA,CAAK,UAAU,MAAM;AAAA;AACnD;AACJ,KACJ;AAAA,EACJ;AAAA,EAEA,MAAM,iBAAA,CACF,MAAA,EACA,SAAA,EACA,IAAA,EACe;AACf,IAAA,MAAM,OAAO,MAAA,CAAO,IAAA;AACpB,IAAA,IAAI,CAAC,MAAM,MAAA,EAAQ;AACf,MAAA,OAAO,MAAA;AAAA,IACX;AAEA,IAAA,MAAM,MAAA,GAASD,2CAAA,CAAY,IAAA,CAAK,MAAM,CAAA;AAEtC,IAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AACxB,MAAA,IAAI,QAAA;AACJ,MAAA,IAAI;AAEA,QAAA,MAAM,MAAA,GAASE,2BAAA,CAAe,KAAA,CAAM,IAAA,EAAM;AAAA,UACtC,WAAA,EAAa,OAAA;AAAA,UACb,gBAAA,EAAkB,MAAA,CAAO,QAAA,CAAS,SAAA,IAAa;AAAA,SAClD,CAAA;AACD,QAAA,QAAA,GAAWC,gCAAmB,MAAM,CAAA;AAAA,MACxC,CAAA,CAAA,MAAQ;AAEJ,QAAA;AAAA,MACJ;AAGA,MAAA,IAAA;AAAA,QACIC,mCAAiB,QAAA,CAAS;AAAA,UACtB,IAAA,EAAMC,8BAAA;AAAA,UACN,MAAA,EAAQ;AAAA,YACJ,MAAM,MAAA,CAAO,IAAA;AAAA,YACb,SAAA,EAAW,MAAA,CAAO,QAAA,CAAS,SAAA,IAAa,SAAA;AAAA,YACxC,IAAA,EAAM,OAAO,QAAA,CAAS;AAAA,WAC1B;AAAA,UACA,MAAA,EAAQH,4BAAe,QAAQ;AAAA,SAClC;AAAA,OACL;AAEA,MAAA,IAAA;AAAA,QACIE,mCAAiB,QAAA,CAAS;AAAA,UACtB,IAAA,EAAME,8BAAA;AAAA,UACN,MAAA,EAAQJ,4BAAe,QAAQ,CAAA;AAAA,UAC/B,MAAA,EAAQ;AAAA,YACJ,MAAM,MAAA,CAAO,IAAA;AAAA,YACb,SAAA,EAAW,MAAA,CAAO,QAAA,CAAS,SAAA,IAAa,SAAA;AAAA,YACxC,IAAA,EAAM,OAAO,QAAA,CAAS;AAAA;AAC1B,SACH;AAAA,OACL;AAAA,IACJ;AAEA,IAAA,OAAO,MAAA;AAAA,EACX;AACJ;;;;"}
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var module$1 = require('./module.cjs.js');
6
+ var MultiOwnerEntitiesProcessor = require('./MultiOwnerEntitiesProcessor.cjs.js');
7
+
8
+
9
+
10
+ exports.default = module$1.default;
11
+ exports.MultiOwnerEntitiesProcessor = MultiOwnerEntitiesProcessor.MultiOwnerEntitiesProcessor;
12
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
@@ -0,0 +1,36 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+ import { CatalogProcessor, CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
3
+ import { Entity } from '@backstage/catalog-model';
4
+ import { LocationSpec } from '@backstage/plugin-catalog-common';
5
+
6
+ /**
7
+ * Backend module that registers the {@link MultiOwnerEntitiesProcessor}
8
+ * with the catalog processing pipeline.
9
+ *
10
+ * @remarks
11
+ * Install this module in your backend by adding:
12
+ * ```ts
13
+ * backend.add(import('@thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor'));
14
+ * ```
15
+ */
16
+ declare const _default: _backstage_backend_plugin_api.BackendFeature;
17
+
18
+ /**
19
+ * A catalog processor that reads `spec.owners` from entities and emits
20
+ * `ownedBy` / `ownerOf` relations for each owner listed.
21
+ *
22
+ * It also writes the normalized owner list as a JSON annotation
23
+ * (`backstage.io/owners`) so the frontend can display them.
24
+ *
25
+ * @remarks
26
+ * This processor runs *in addition to* the built-in processor that handles
27
+ * `spec.owner`. If both fields are present, owners from both are emitted.
28
+ * Duplicate relations are automatically deduplicated by the catalog engine.
29
+ */
30
+ declare class MultiOwnerEntitiesProcessor implements CatalogProcessor {
31
+ getProcessorName(): string;
32
+ preProcessEntity(entity: Entity, _location: LocationSpec): Promise<Entity>;
33
+ postProcessEntity(entity: Entity, _location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
34
+ }
35
+
36
+ export { MultiOwnerEntitiesProcessor, _default as default };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var backendPluginApi = require('@backstage/backend-plugin-api');
6
+ var alpha = require('@backstage/plugin-catalog-node/alpha');
7
+ var MultiOwnerEntitiesProcessor = require('./MultiOwnerEntitiesProcessor.cjs.js');
8
+
9
+ var module$1 = backendPluginApi.createBackendModule({
10
+ pluginId: "catalog",
11
+ moduleId: "multi-owner-processor",
12
+ register(reg) {
13
+ reg.registerInit({
14
+ deps: {
15
+ catalog: alpha.catalogProcessingExtensionPoint
16
+ },
17
+ async init({ catalog }) {
18
+ catalog.addProcessor(new MultiOwnerEntitiesProcessor.MultiOwnerEntitiesProcessor());
19
+ }
20
+ });
21
+ }
22
+ });
23
+
24
+ exports.default = module$1;
25
+ //# sourceMappingURL=module.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.cjs.js","sources":["../src/module.ts"],"sourcesContent":["import { createBackendModule } from '@backstage/backend-plugin-api';\nimport { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';\nimport { MultiOwnerEntitiesProcessor } from './MultiOwnerEntitiesProcessor';\n\n/**\n * Backend module that registers the {@link MultiOwnerEntitiesProcessor}\n * with the catalog processing pipeline.\n *\n * @remarks\n * Install this module in your backend by adding:\n * ```ts\n * backend.add(import('@thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor'));\n * ```\n */\nexport default createBackendModule({\n pluginId: 'catalog',\n moduleId: 'multi-owner-processor',\n register(reg) {\n reg.registerInit({\n deps: {\n catalog: catalogProcessingExtensionPoint,\n },\n async init({ catalog }) {\n catalog.addProcessor(new MultiOwnerEntitiesProcessor());\n },\n });\n },\n});\n"],"names":["createBackendModule","catalogProcessingExtensionPoint","MultiOwnerEntitiesProcessor"],"mappings":";;;;;;;;AAcA,eAAeA,oCAAA,CAAoB;AAAA,EAC/B,QAAA,EAAU,SAAA;AAAA,EACV,QAAA,EAAU,uBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACV,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACb,IAAA,EAAM;AAAA,QACF,OAAA,EAASC;AAAA,OACb;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,OAAA,EAAQ,EAAG;AACpB,QAAA,OAAA,CAAQ,YAAA,CAAa,IAAIC,uDAAA,EAA6B,CAAA;AAAA,MAC1D;AAAA,KACH,CAAA;AAAA,EACL;AACJ,CAAC,CAAA;;;;"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor",
3
+ "version": "1.0.2",
4
+ "license": "Apache-2.0",
5
+ "main": "./dist/index.cjs.js",
6
+ "types": "./dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "backstage": {
11
+ "role": "backend-plugin-module",
12
+ "pluginId": "catalog",
13
+ "pluginPackage": "@backstage/plugin-catalog-backend",
14
+ "pluginPackages": [
15
+ "@thecodingsheikh/backstage-plugin-multi-owner-common",
16
+ "@thecodingsheikh/backstage-plugin-multi-owner",
17
+ "@thecodingsheikh/backstage-plugin-catalog-backend-module-multi-owner-processor"
18
+ ],
19
+ "features": {
20
+ ".": "@backstage/BackendFeature"
21
+ }
22
+ },
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "start": "backstage-cli package start",
26
+ "build": "backstage-cli package build",
27
+ "lint": "backstage-cli package lint",
28
+ "test": "backstage-cli package test",
29
+ "clean": "backstage-cli package clean",
30
+ "prepack": "backstage-cli package prepack",
31
+ "postpack": "backstage-cli package postpack"
32
+ },
33
+ "dependencies": {
34
+ "@backstage/backend-plugin-api": "^1.1.1",
35
+ "@backstage/catalog-model": "^1.7.3",
36
+ "@backstage/plugin-catalog-common": "^1.1.2",
37
+ "@backstage/plugin-catalog-node": "^1.15.1",
38
+ "@thecodingsheikh/backstage-plugin-multi-owner-common": "^1.0.2"
39
+ },
40
+ "devDependencies": {
41
+ "@backstage/backend-test-utils": "^1.2.1",
42
+ "@backstage/cli": "^0.34.5"
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "typesVersions": {
48
+ "*": {
49
+ "package.json": [
50
+ "package.json"
51
+ ]
52
+ }
53
+ }
54
+ }