diff --git a/.changeset/clean-queens-judge.md b/.changeset/clean-queens-judge.md new file mode 100644 index 0000000000..22f87e6959 --- /dev/null +++ b/.changeset/clean-queens-judge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Refactored catalog collator, extracted abstract class and entity processor to make it possible to re-use functionality diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index a46abda5d3..8432092796 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -167,6 +167,42 @@ export class CatalogBuilder { useLegacySingleProcessorValidation(): this; } +// @public (undocumented) +export abstract class CatalogCollatorFactory + implements DocumentCollatorFactory +{ + protected constructor(options: CatalogCollatorFactoryOptions); + // (undocumented) + static fromConfig( + _config: Config, + _options: CatalogCollatorFactoryCreateOptions, + ): CatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public (undocumented) +export type CatalogCollatorFactoryCreateOptions = Omit< + CatalogCollatorFactoryOptions, + 'entityProcessor' | 'type' +>; + +// @public (undocumented) +export type CatalogCollatorFactoryOptions = { + type: string; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + entityProcessor: CatalogCollatorEntityProcessor; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + // @alpha export const catalogConditions: Conditions<{ hasAnnotation: PermissionRule< @@ -351,29 +387,17 @@ export class DefaultCatalogCollator { } // @public (undocumented) -export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { +export class DefaultCatalogCollatorFactory extends CatalogCollatorFactory { // (undocumented) static fromConfig( _config: Config, options: DefaultCatalogCollatorFactoryOptions, ): DefaultCatalogCollatorFactory; - // (undocumented) - getCollator(): Promise; - // (undocumented) - readonly type: string; - // (undocumented) - readonly visibilityPermission: Permission; } // @public (undocumented) -export type DefaultCatalogCollatorFactoryOptions = { - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - locationTemplate?: string; - filter?: GetEntitiesRequest['filter']; - batchSize?: number; - catalogClient?: CatalogApi; -}; +export type DefaultCatalogCollatorFactoryOptions = + CatalogCollatorFactoryCreateOptions; export { DeferredEntity }; @@ -583,4 +607,8 @@ export class UrlReaderProcessor implements CatalogProcessor { cache: CatalogProcessorCache, ): Promise; } + +// Warnings were encountered during analysis: +// +// src/search/CatalogCollatorFactory.d.ts:14:5 - (ae-forgotten-export) The symbol "CatalogCollatorEntityProcessor" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog-backend/src/search/util.ts b/plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts similarity index 58% rename from plugins/catalog-backend/src/search/util.ts rename to plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts index fd54d397c9..0288cbec24 100644 --- a/plugins/catalog-backend/src/search/util.ts +++ b/plugins/catalog-backend/src/search/CatalogCollatorEntityProcessor.ts @@ -14,16 +14,9 @@ * limitations under the License. */ -import { Entity, isUserEntity, isGroupEntity } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; -export function getDocumentText(entity: Entity): string { - const documentTexts: string[] = []; - documentTexts.push(entity.metadata.description || ''); - - if (isUserEntity(entity) || isGroupEntity(entity)) { - if (entity.spec?.profile?.displayName) { - documentTexts.push(entity.spec.profile.displayName); - } - } - return documentTexts.join(' : '); +export interface CatalogCollatorEntityProcessor { + process(entity: Entity, locationTemplate: string): CatalogEntityDocument; } diff --git a/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts new file mode 100644 index 0000000000..8b9a60b264 --- /dev/null +++ b/plugins/catalog-backend/src/search/CatalogCollatorFactory.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; +import { Permission } from '@backstage/plugin-permission-common'; +import { Readable } from 'stream'; +import { CatalogCollatorEntityProcessor } from './CatalogCollatorEntityProcessor'; +import { Config } from '@backstage/config'; + +/** @public */ +export type CatalogCollatorFactoryOptions = { + type: string; + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + entityProcessor: CatalogCollatorEntityProcessor; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + +/** @public */ +export type CatalogCollatorFactoryCreateOptions = Omit< + CatalogCollatorFactoryOptions, + 'entityProcessor' | 'type' +>; + +/** @public */ +export abstract class CatalogCollatorFactory + implements DocumentCollatorFactory +{ + public readonly type: string; + public readonly visibilityPermission: Permission = + catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private tokenManager: TokenManager; + private entityProcessor: CatalogCollatorEntityProcessor; + + static fromConfig( + _config: Config, + _options: CatalogCollatorFactoryCreateOptions, + ): CatalogCollatorFactory { + throw new Error('Method should be implemented'); + } + + protected constructor(options: CatalogCollatorFactoryOptions) { + const { + type, + batchSize, + discovery, + locationTemplate, + filter, + catalogClient, + tokenManager, + entityProcessor, + } = options; + + this.type = type; + this.locationTemplate = + locationTemplate || '/catalog/:namespace/:kind/:name'; + this.filter = filter; + this.batchSize = batchSize || 500; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; + this.entityProcessor = entityProcessor; + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: this.filter, + limit: this.batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === this.batchSize; + entitiesRetrieved += entities.length; + + for (const entity of entities) { + yield this.entityProcessor.process(entity, this.locationTemplate); + } + } + } +} diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts new file mode 100644 index 0000000000..1183d5be97 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DefaultCatalogCollatorEntityProcessor } from './DefaultCatalogCollatorEntityProcessor'; + +const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity', + description: 'The expected description', + namespace: 'namespace', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, +}; + +const userEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'test-user-entity', + description: 'The expected user description', + }, + spec: { + profile: { + displayName: 'User 1', + }, + }, +}; + +const locationTemplate = '/catalog/:namespace/:kind/:name'; + +describe('DefaultCatalogCollatorEntityProcessor', () => { + const entityProcessor = new DefaultCatalogCollatorEntityProcessor(); + + describe('process', () => { + it('maps a returned entity', async () => { + const document = entityProcessor.process(entity, locationTemplate); + + expect(document).toMatchObject({ + title: entity.metadata.title, + location: '/catalog/namespace/component/test-entity', + text: entity.metadata.description, + namespace: entity.metadata.namespace, + componentType: entity.spec.type, + lifecycle: entity.spec.lifecycle, + owner: entity.spec.owner, + authorization: { + resourceRef: 'component:namespace/test-entity', + }, + }); + }); + + it('maps a returned entity with default fallback', async () => { + const entityWithoutTitle = { + ...entity, + metadata: { + ...entity.metadata, + title: undefined, + namespace: undefined, + }, + spec: { + type: undefined, + lifecycle: undefined, + owner: undefined, + }, + }; + + const document = entityProcessor.process( + entityWithoutTitle, + locationTemplate, + ); + + expect(document).toMatchObject({ + title: entity.metadata.name, + location: '/catalog/default/component/test-entity', + text: entity.metadata.description, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + }); + + it('maps a returned entity with custom locationTemplate', async () => { + const document = entityProcessor.process(entity, '/catalog/:name'); + + expect(document).toMatchObject({ + title: entity.metadata.title, + location: '/catalog/test-entity', + text: entity.metadata.description, + namespace: entity.metadata.namespace, + componentType: entity.spec.type, + lifecycle: entity.spec.lifecycle, + owner: entity.spec.owner, + authorization: { + resourceRef: 'component:namespace/test-entity', + }, + }); + }); + + it('maps a returned user entity', async () => { + const document = entityProcessor.process(userEntity, locationTemplate); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + location: '/catalog/default/user/test-user-entity', + text: `${userEntity.metadata.description} : ${userEntity.spec.profile.displayName}`, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'user:default/test-user-entity', + }, + }); + }); + + it('maps a returned user entity without display name', async () => { + const testEntity = { + ...userEntity, + spec: undefined, + }; + + const document = entityProcessor.process(testEntity, locationTemplate); + + expect(document).toMatchObject({ + title: userEntity.metadata.name, + location: '/catalog/default/user/test-user-entity', + text: userEntity.metadata.description, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'user:default/test-user-entity', + }, + }); + }); + + it('maps a returned group entity', async () => { + const groupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'test-group-entity', + description: 'The expected group description', + }, + spec: { + profile: { + displayName: 'Group 1', + }, + }, + }; + + const document = entityProcessor.process(groupEntity, locationTemplate); + + expect(document).toMatchObject({ + title: groupEntity.metadata.name, + location: '/catalog/default/group/test-group-entity', + text: `${groupEntity.metadata.description} : ${groupEntity.spec.profile.displayName}`, + namespace: 'default', + componentType: 'other', + lifecycle: '', + owner: '', + authorization: { + resourceRef: 'group:default/test-group-entity', + }, + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts new file mode 100644 index 0000000000..3078ed4310 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorEntityProcessor.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + isGroupEntity, + isUserEntity, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; +import { CatalogCollatorEntityProcessor } from './CatalogCollatorEntityProcessor'; + +export class DefaultCatalogCollatorEntityProcessor + implements CatalogCollatorEntityProcessor +{ + public process( + entity: Entity, + locationTemplate: string, + ): CatalogEntityDocument { + return { + title: entity.metadata.title ?? entity.metadata.name, + location: this.applyArgsToFormat(locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + text: this.getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + }; + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + + return formatted.toLowerCase(); + } + + private getDocumentText(entity: Entity): string { + const documentTexts: string[] = []; + documentTexts.push(entity.metadata.description || ''); + + if (isUserEntity(entity) || isGroupEntity(entity)) { + if (entity.spec?.profile?.displayName) { + documentTexts.push(entity.spec.profile.displayName); + } + } + + return documentTexts.join(' : '); + } +} diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index cf082c271c..8fbc2bef60 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -14,133 +14,27 @@ * limitations under the License. */ -import { - PluginEndpointDiscovery, - TokenManager, -} from '@backstage/backend-common'; -import { - CatalogApi, - CatalogClient, - GetEntitiesRequest, -} from '@backstage/catalog-client'; -import { stringifyEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { - catalogEntityReadPermission, - CatalogEntityDocument, -} from '@backstage/plugin-catalog-common'; -import { Permission } from '@backstage/plugin-permission-common'; -import { Readable } from 'stream'; -import { getDocumentText } from './util'; + CatalogCollatorFactory, + CatalogCollatorFactoryCreateOptions, +} from './CatalogCollatorFactory'; +import { DefaultCatalogCollatorEntityProcessor } from './DefaultCatalogCollatorEntityProcessor'; /** @public */ -export type DefaultCatalogCollatorFactoryOptions = { - discovery: PluginEndpointDiscovery; - tokenManager: TokenManager; - locationTemplate?: string; - filter?: GetEntitiesRequest['filter']; - batchSize?: number; - catalogClient?: CatalogApi; -}; +export type DefaultCatalogCollatorFactoryOptions = + CatalogCollatorFactoryCreateOptions; /** @public */ -export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { - public readonly type: string = 'software-catalog'; - public readonly visibilityPermission: Permission = - catalogEntityReadPermission; - - private locationTemplate: string; - private filter?: GetEntitiesRequest['filter']; - private batchSize: number; - private readonly catalogClient: CatalogApi; - private tokenManager: TokenManager; - +export class DefaultCatalogCollatorFactory extends CatalogCollatorFactory { static fromConfig( _config: Config, options: DefaultCatalogCollatorFactoryOptions, ) { - return new DefaultCatalogCollatorFactory(options); - } - - private constructor(options: DefaultCatalogCollatorFactoryOptions) { - const { - batchSize, - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - } = options; - - this.locationTemplate = - locationTemplate || '/catalog/:namespace/:kind/:name'; - this.filter = filter; - this.batchSize = batchSize || 500; - this.catalogClient = - catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.tokenManager = tokenManager; - } - - async getCollator(): Promise { - return Readable.from(this.execute()); - } - - private applyArgsToFormat( - format: string, - args: Record, - ): string { - let formatted = format; - for (const [key, value] of Object.entries(args)) { - formatted = formatted.replace(`:${key}`, value); - } - return formatted.toLowerCase(); - } - - private async *execute(): AsyncGenerator { - const { token } = await this.tokenManager.getToken(); - let entitiesRetrieved = 0; - let moreEntitiesToGet = true; - - // Offset/limit pagination is used on the Catalog Client in order to - // limit (and allow some control over) memory used by the search backend - // at index-time. - while (moreEntitiesToGet) { - const entities = ( - await this.catalogClient.getEntities( - { - filter: this.filter, - limit: this.batchSize, - offset: entitiesRetrieved, - }, - { token }, - ) - ).items; - - // Control looping through entity batches. - moreEntitiesToGet = entities.length === this.batchSize; - entitiesRetrieved += entities.length; - - for (const entity of entities) { - yield { - title: entity.metadata.title ?? entity.metadata.name, - location: this.applyArgsToFormat(this.locationTemplate, { - namespace: entity.metadata.namespace || 'default', - kind: entity.kind, - name: entity.metadata.name, - }), - text: getDocumentText(entity), - componentType: entity.spec?.type?.toString() || 'other', - type: entity.spec?.type?.toString() || 'other', - namespace: entity.metadata.namespace || 'default', - kind: entity.kind, - lifecycle: (entity.spec?.lifecycle as string) || '', - owner: (entity.spec?.owner as string) || '', - authorization: { - resourceRef: stringifyEntityRef(entity), - }, - }; - } - } + return new DefaultCatalogCollatorFactory({ + ...options, + type: 'software-catalog', + entityProcessor: new DefaultCatalogCollatorEntityProcessor(), + }); } } diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 2602f0fada..fd025ddca4 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -16,6 +16,11 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; +export { CatalogCollatorFactory } from './CatalogCollatorFactory'; +export type { + CatalogCollatorFactoryOptions, + CatalogCollatorFactoryCreateOptions, +} from './CatalogCollatorFactory'; /** * todo(backstage/techdocs-core): stop exporting this in a future release. diff --git a/plugins/catalog-backend/src/search/util.test.ts b/plugins/catalog-backend/src/search/util.test.ts deleted file mode 100644 index d2d1776338..0000000000 --- a/plugins/catalog-backend/src/search/util.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ComponentEntity, - GroupEntity, - UserEntity, -} from '@backstage/catalog-model'; -import { getDocumentText } from './util'; - -describe('getDocumentText', () => { - describe('kind is not User or Group', () => { - test('contains description if set', () => { - const entity = createComponent(); - entity.metadata.description = 'The expected description'; - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description is not set', () => { - const entity = createComponent(); - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); - - describe('kind is User', () => { - test('contains display name if set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - }); - - test('contains description if set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('contains both description and display name if both are set', () => { - const entity = createUser(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description and display name are not set', () => { - const entity = createUser(); - delete entity.metadata.description; - delete entity.spec.profile?.displayName; - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); - - describe('kind is Group', () => { - test('contains display name if set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - }); - - test('contains description if set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.metadata.description); - }); - - test('contains both description and display name if both are set', () => { - const entity = createGroup(); - const actual = getDocumentText(entity); - expect(actual).toContain(entity.spec.profile?.displayName); - expect(actual).toContain(entity.metadata.description); - }); - - test('is empty if description and display name are not set', () => { - const entity = createGroup(); - delete entity.metadata.description; - delete entity.spec.profile?.displayName; - const actual = getDocumentText(entity); - expect(actual).toEqual(''); - }); - }); -}); - -function createGroup(): GroupEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: 'group-1', - description: 'The expected description', - }, - spec: { - type: 'team', - profile: { - displayName: 'Group 1', - }, - children: [], - }, - }; -} - -function createUser(): UserEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: 'user-1', - description: 'The expected description', - }, - spec: { - profile: { - displayName: 'User 1', - }, - }, - }; -} - -function createComponent(): ComponentEntity { - return { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'component-1', - }, - spec: { - lifecycle: 'experimental', - owner: 'someone', - type: 'service', - }, - }; -}