Refactored TechDocsCollator; added
entityTransformer functionality Signed-off-by: Hannes Jetter <hannes.jetter@aeb.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-techdocs': patch
|
||||
---
|
||||
|
||||
The process of adding or modifying fields in the techdocs search index has been simplified. For more details, see [How to customize fields in the Software Catalog or TechDocs index](../docs/features/search/how-to-guides.md#how-to-customize-fields-in-the-software-catalog-or-techdocs-index).
|
||||
@@ -114,18 +114,18 @@ of the `SearchType` component.
|
||||
|
||||
> Check out the documentation around [integrating search into plugins](../../plugins/integrating-search-into-plugins.md#create-a-collator) for how to create your own collator.
|
||||
|
||||
## How to customize fields in the Software Catalog index
|
||||
## How to customize fields in the Software Catalog or TechDocs index
|
||||
|
||||
Sometimes you will might want to have ability to control
|
||||
which data passes to search index in catalog collator, or to customize data for specific kind.
|
||||
You can easily do that by passing `entityTransformer` callback to `DefaultCatalogCollatorFactory`.
|
||||
You can either just simply amend default behaviour, or even to write completely new document
|
||||
(which should follow some required basic structure though).
|
||||
Sometimes, you might want to have the ability to control which data passes into the search index
|
||||
in the catalog collator or customize data for a specific kind. You can easily achieve this
|
||||
by passing an `entityTransformer` callback to the `DefaultCatalogCollatorFactory`. This behavior
|
||||
is also possible for the `DefaultTechDocsCollatorFactory`. You can either simply amend the default behavior
|
||||
or even write an entirely new document (which should still follow some required basic structure).
|
||||
|
||||
> `authorization` and `location` cannot be modified via a `entityTransformer`, `location` can be modified only through `locationTemplate`.
|
||||
|
||||
```ts title="packages/backend/src/plugins/search.ts"
|
||||
const entityTransformer: CatalogCollatorEntityTransformer = (
|
||||
const catalogEntityTransformer: CatalogCollatorEntityTransformer = (
|
||||
entity: Entity,
|
||||
) => {
|
||||
if (entity.kind === 'SomeKind') {
|
||||
@@ -146,7 +146,26 @@ indexBuilder.addCollator({
|
||||
discovery: env.discovery,
|
||||
tokenManager: env.tokenManager,
|
||||
/* highlight-add-next-line */
|
||||
entityTransformer,
|
||||
entityTransformer: catalogEntityTransformer,
|
||||
}),
|
||||
});
|
||||
|
||||
const techDocsEntityTransformer: TechDocsCollatorEntityTransformer = (
|
||||
entity: Entity,
|
||||
) => {
|
||||
return {
|
||||
// add more fields to the index
|
||||
...defaultTechDocsCollatorEntityTransformer(entity),
|
||||
tags: entity.metadata.tags,
|
||||
};
|
||||
};
|
||||
|
||||
indexBuilder.addCollator({
|
||||
collator: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
|
||||
discovery: env.discovery,
|
||||
tokenManager: env.tokenManager,
|
||||
/* highlight-add-next-line */
|
||||
entityTransformer: techDocsEntityTransformer,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
+41
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* Copyright 2023 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.
|
||||
@@ -26,6 +26,8 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { Readable } from 'stream';
|
||||
import { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory';
|
||||
import { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer';
|
||||
import { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -66,6 +68,7 @@ const expectedEntities: Entity[] = [
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': './',
|
||||
},
|
||||
tags: ['tag1', 'tag2'],
|
||||
},
|
||||
spec: {
|
||||
type: 'dog',
|
||||
@@ -256,5 +259,42 @@ describe('DefaultTechDocsCollatorFactory', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform the entity using the entityTransformer function', async () => {
|
||||
const entityTransformer: TechDocsCollatorEntityTransformer = (
|
||||
entity: Entity,
|
||||
) => {
|
||||
return {
|
||||
...defaultTechDocsCollatorEntityTransformer(entity),
|
||||
tags: entity.metadata.tags,
|
||||
};
|
||||
};
|
||||
|
||||
factory = DefaultTechDocsCollatorFactory.fromConfig(config, {
|
||||
...options,
|
||||
entityTransformer,
|
||||
});
|
||||
|
||||
collator = await factory.getCollator();
|
||||
|
||||
const pipeline = TestPipeline.fromCollator(collator);
|
||||
const { documents } = await pipeline.execute();
|
||||
const entity = expectedEntities[0];
|
||||
documents.forEach((document, idx) => {
|
||||
expect(document).toMatchObject({
|
||||
title: mockSearchDocIndex.docs[idx].title,
|
||||
location: `/docs/default/component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`,
|
||||
text: mockSearchDocIndex.docs[idx].text,
|
||||
namespace: 'default',
|
||||
entityTitle: entity!.metadata.title,
|
||||
componentType: entity!.spec!.type,
|
||||
lifecycle: entity!.spec!.lifecycle,
|
||||
owner: '',
|
||||
kind: entity.kind.toLocaleLowerCase('en-US'),
|
||||
name: entity.metadata.name,
|
||||
tags: entity.metadata.tags,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+8
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* Copyright 2023 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.
|
||||
@@ -39,6 +39,8 @@ import fetch from 'node-fetch';
|
||||
import pLimit from 'p-limit';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer';
|
||||
import { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer';
|
||||
|
||||
interface MkSearchIndexDoc {
|
||||
title: string;
|
||||
@@ -59,6 +61,7 @@ export type TechDocsCollatorFactoryOptions = {
|
||||
catalogClient?: CatalogApi;
|
||||
parallelismLimit?: number;
|
||||
legacyPathCasing?: boolean;
|
||||
entityTransformer?: TechDocsCollatorEntityTransformer;
|
||||
};
|
||||
|
||||
type EntityInfo = {
|
||||
@@ -85,6 +88,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {
|
||||
private readonly tokenManager: TokenManager;
|
||||
private readonly parallelismLimit: number;
|
||||
private readonly legacyPathCasing: boolean;
|
||||
private entityTransformer: TechDocsCollatorEntityTransformer;
|
||||
|
||||
private constructor(options: TechDocsCollatorFactoryOptions) {
|
||||
this.discovery = options.discovery;
|
||||
@@ -97,6 +101,8 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {
|
||||
this.parallelismLimit = options.parallelismLimit ?? 10;
|
||||
this.legacyPathCasing = options.legacyPathCasing ?? false;
|
||||
this.tokenManager = options.tokenManager;
|
||||
this.entityTransformer =
|
||||
options.entityTransformer ?? defaultTechDocsCollatorEntityTransformer;
|
||||
}
|
||||
|
||||
static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions) {
|
||||
@@ -142,17 +148,6 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {
|
||||
'metadata.annotations.backstage.io/techdocs-ref':
|
||||
CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
fields: [
|
||||
'kind',
|
||||
'namespace',
|
||||
'metadata.annotations',
|
||||
'metadata.name',
|
||||
'metadata.title',
|
||||
'metadata.namespace',
|
||||
'spec.type',
|
||||
'spec.lifecycle',
|
||||
'relations',
|
||||
],
|
||||
limit: batchSize,
|
||||
offset: entitiesRetrieved,
|
||||
},
|
||||
@@ -204,6 +199,7 @@ export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {
|
||||
]);
|
||||
|
||||
return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({
|
||||
...this.entityTransformer(entity),
|
||||
title: unescape(doc.title),
|
||||
text: unescape(doc.text || ''),
|
||||
location: this.applyArgsToFormat(
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2023 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 } from '@backstage/catalog-model';
|
||||
import { TechDocsDocument } from '@backstage/plugin-techdocs-node';
|
||||
|
||||
/** @public */
|
||||
export type TechDocsCollatorEntityTransformer = (
|
||||
entity: Entity,
|
||||
) => Omit<TechDocsDocument, 'location' | 'authorization'>;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 } from '@backstage/catalog-model';
|
||||
import { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer';
|
||||
|
||||
describe('defaultTechDocsCollatorEntityTransformer', () => {
|
||||
it('should transform the entity with the correct properties', () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test-component',
|
||||
description: 'A test component',
|
||||
annotations: {
|
||||
'backstage.io/techdocs-ref': 'docs',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
owner: 'test@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
const transformedEntity = defaultTechDocsCollatorEntityTransformer(entity);
|
||||
|
||||
expect(transformedEntity).toEqual({
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
annotations: entity.metadata.annotations || '',
|
||||
name: entity.metadata.name || '',
|
||||
title: entity.metadata.title || '',
|
||||
text: 'A test component',
|
||||
componentType: entity.spec?.type?.toString() || 'other',
|
||||
type: entity.spec?.type?.toString() || 'other',
|
||||
lifecycle: (entity.spec?.lifecycle as string) || '',
|
||||
owner: (entity.spec?.owner as string) || '',
|
||||
path: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2023 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 } from '@backstage/catalog-model';
|
||||
import { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer';
|
||||
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (isUserEntity(entity)) {
|
||||
if (entity.spec?.profile?.email) {
|
||||
documentTexts.push(entity.spec.profile.email);
|
||||
}
|
||||
}
|
||||
|
||||
return documentTexts.join(' : ');
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const defaultTechDocsCollatorEntityTransformer: TechDocsCollatorEntityTransformer =
|
||||
(entity: Entity) => {
|
||||
return {
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
annotations: entity.metadata.annotations || '',
|
||||
name: entity.metadata.name || '',
|
||||
title: entity.metadata.title || '',
|
||||
text: getDocumentText(entity),
|
||||
componentType: entity.spec?.type?.toString() || 'other',
|
||||
type: entity.spec?.type?.toString() || 'other',
|
||||
lifecycle: (entity.spec?.lifecycle as string) || '',
|
||||
owner: (entity.spec?.owner as string) || '',
|
||||
path: '',
|
||||
};
|
||||
};
|
||||
@@ -17,3 +17,7 @@
|
||||
export { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory';
|
||||
|
||||
export type { TechDocsCollatorFactoryOptions } from './DefaultTechDocsCollatorFactory';
|
||||
|
||||
export { defaultTechDocsCollatorEntityTransformer } from './defaultTechDocsCollatorEntityTransformer';
|
||||
|
||||
export type { TechDocsCollatorEntityTransformer } from './TechDocsCollatorEntityTransformer';
|
||||
|
||||
Reference in New Issue
Block a user