Migrate techdocs plugins from alpha catalogServiceRef to stable

Migrated `@backstage/plugin-techdocs-backend` and
`@backstage/plugin-search-backend-module-techdocs` to use the stable
`catalogServiceRef` from `@backstage/plugin-catalog-node` instead of
the deprecated one from `@backstage/plugin-catalog-node/alpha`.

This also updates `CachedEntityLoader`, `DefaultTechDocsCollatorFactory`,
and the TechDocs router to use `CatalogService` (credentials-based) instead
of `CatalogApi` (token-based).

Signed-off-by: Fredrik Adelöw <freben@spotify.com>
Made-with: Cursor
This commit is contained in:
Fredrik Adelöw
2026-04-03 22:53:27 +02:00
parent c59dccbaf5
commit 5e32f77884
9 changed files with 52 additions and 89 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ import {
techdocsPreparerExtensionPoint,
techdocsPublisherExtensionPoint,
} from '@backstage/plugin-techdocs-node';
import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import * as winston from 'winston';
import { createRouter } from './service/router';
@@ -39,8 +39,6 @@ describe('CachedEntityLoader', () => {
},
};
const token = 'test-token';
const userCredentials: BackstageCredentials = {
$$type: '@backstage/BackstageCredentials',
principal: {
@@ -67,7 +65,7 @@ describe('CachedEntityLoader', () => {
auth.isPrincipal.mockReturnValue(true);
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(userCredentials, entityName, token);
const result = await loader.load(userCredentials, entityName);
expect(result).toEqual(entity);
expect(cache.set).toHaveBeenCalledWith(
@@ -84,7 +82,7 @@ describe('CachedEntityLoader', () => {
auth.isPrincipal.mockReturnValue(true);
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(userCredentials, entityName, token);
const result = await loader.load(userCredentials, entityName);
expect(result).toEqual(entity);
expect(catalog.getEntityByRef).not.toHaveBeenCalled();
@@ -96,7 +94,7 @@ describe('CachedEntityLoader', () => {
auth.isPrincipal.mockReturnValue(true);
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(userCredentials, entityName, token);
const result = await loader.load(userCredentials, entityName);
expect(result).toBeUndefined();
expect(cache.set).not.toHaveBeenCalled();
@@ -108,7 +106,7 @@ describe('CachedEntityLoader', () => {
auth.isPrincipal.mockReturnValueOnce(false).mockReturnValueOnce(true);
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(pluginCredentials, entityName, undefined);
const result = await loader.load(pluginCredentials, entityName);
expect(result).toEqual(entity);
expect(cache.set).toHaveBeenCalledWith(
@@ -131,7 +129,7 @@ describe('CachedEntityLoader', () => {
auth.isPrincipal.mockReturnValue(true);
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(userCredentials, entityName, token);
const result = await loader.load(userCredentials, entityName);
expect(result).toEqual(entity);
});
@@ -151,8 +149,8 @@ describe('CachedEntityLoader', () => {
},
};
await loader.load(userCredentials, entityName, token);
await loader.load(anotherUserCredentials, entityName, token);
await loader.load(userCredentials, entityName);
await loader.load(anotherUserCredentials, entityName);
expect(cache.set).toHaveBeenCalledWith(
'catalog:component:default/test:user:default/test-user',
@@ -172,7 +170,7 @@ describe('CachedEntityLoader', () => {
auth.isPrincipal.mockReturnValueOnce(false).mockReturnValueOnce(true);
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(pluginCredentials, entityName, token);
const result = await loader.load(pluginCredentials, entityName);
expect(result).toEqual(entity);
expect(cache.set).toHaveBeenCalledWith(
@@ -197,7 +195,7 @@ describe('CachedEntityLoader', () => {
};
const loader = new CachedEntityLoader({ auth, catalog, cache });
const result = await loader.load(unknownCredentials, entityName, token);
const result = await loader.load(unknownCredentials, entityName);
expect(result).toEqual(entity);
expect(cache.set).toHaveBeenCalledWith(
@@ -19,22 +19,22 @@ import {
BackstageCredentials,
CacheService,
} from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import {
Entity,
CompoundEntityRef,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { CatalogService } from '@backstage/plugin-catalog-node';
export type CachedEntityLoaderOptions = {
auth: AuthService;
catalog: CatalogApi;
catalog: CatalogService;
cache: CacheService;
};
export class CachedEntityLoader {
private readonly auth: AuthService;
private readonly catalog: CatalogApi;
private readonly catalog: CatalogService;
private readonly cache: CacheService;
private readonly readTimeout = 1000;
@@ -47,7 +47,6 @@ export class CachedEntityLoader {
async load(
credentials: BackstageCredentials,
entityRef: CompoundEntityRef,
token: string | undefined,
): Promise<Entity | undefined> {
const cacheKey = this.getCacheKey(entityRef, credentials);
let result = await this.getFromCache(cacheKey);
@@ -56,7 +55,7 @@ export class CachedEntityLoader {
return result;
}
result = await this.catalog.getEntityByRef(entityRef, { token });
result = await this.catalog.getEntityByRef(entityRef, { credentials });
if (result) {
this.cache.set(cacheKey, result, { ttl: 5000 });
@@ -28,8 +28,8 @@ import { CachedEntityLoader } from './CachedEntityLoader';
import { createEventStream, createRouter, RouterOptions } from './router';
import { TechDocsCache } from '../cache';
import { mockErrorHandler, mockServices } from '@backstage/backend-test-utils';
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
jest.mock('@backstage/catalog-client');
jest.mock('./CachedEntityLoader');
jest.mock('./DocsSynchronizer');
jest.mock('../cache/TechDocsCache');
@@ -107,6 +107,7 @@ describe('createRouter', () => {
const docsBuildStrategy: jest.Mocked<DocsBuildStrategy> = {
shouldBuild: jest.fn(),
};
const mockCatalogService = catalogServiceMock();
const outOfTheBoxOptions = {
preparers,
generators,
@@ -124,6 +125,7 @@ describe('createRouter', () => {
docsBuildStrategy,
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
catalogClient: mockCatalogService,
};
const recommendedOptions = {
publisher,
@@ -134,6 +136,7 @@ describe('createRouter', () => {
docsBuildStrategy,
auth: mockServices.auth(),
httpAuth: mockServices.httpAuth(),
catalogClient: mockCatalogService,
};
beforeEach(() => {
+14 -32
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { Config, readDurationFromConfig } from '@backstage/config';
import { NotFoundError } from '@backstage/errors';
@@ -41,6 +40,7 @@ import {
HttpAuthService,
LoggerService,
} from '@backstage/backend-plugin-api';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { durationToMilliseconds } from '@backstage/types';
/**
@@ -60,7 +60,7 @@ export type OutOfTheBoxDeploymentOptions = {
cache: CacheService;
docsBuildStrategy?: DocsBuildStrategy;
buildLogTransport?: winston.transport;
catalogClient?: CatalogApi;
catalogClient?: CatalogService;
httpAuth: HttpAuthService;
auth: AuthService;
};
@@ -79,7 +79,7 @@ export type RecommendedDeploymentOptions = {
cache: CacheService;
docsBuildStrategy?: DocsBuildStrategy;
buildLogTransport?: winston.transport;
catalogClient?: CatalogApi;
catalogClient?: CatalogService;
httpAuth: HttpAuthService;
auth: AuthService;
};
@@ -116,8 +116,10 @@ export async function createRouter(
const router = Router();
const { publisher, config, logger, discovery, httpAuth, auth } = options;
const catalogClient =
options.catalogClient ?? new CatalogClient({ discoveryApi: discovery });
if (!options.catalogClient) {
throw new Error('catalogClient is required');
}
const catalogClient = options.catalogClient;
const docsBuildStrategy =
options.docsBuildStrategy ?? DefaultDocsBuildStrategy.fromConfig(config);
const buildLogTransport = options.buildLogTransport;
@@ -163,13 +165,8 @@ export async function createRouter(
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
// Verify that the related entity exists and the current user has permission to view it.
const entity = await entityLoader.load(credentials, entityName, token);
const entity = await entityLoader.load(credentials, entityName);
if (!entity) {
throw new NotFoundError(
@@ -202,12 +199,7 @@ export async function createRouter(
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const entity = await entityLoader.load(credentials, entityName, token);
const entity = await entityLoader.load(credentials, entityName);
if (!entity) {
throw new NotFoundError(
@@ -240,17 +232,12 @@ export async function createRouter(
const credentials = await httpAuth.credentials(req);
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
const entity = await entityLoader.load(credentials, {
kind,
namespace,
name,
});
const entity = await entityLoader.load(
credentials,
{ kind, namespace, name },
token,
);
if (!entity?.metadata?.uid) {
throw new NotFoundError('Entity metadata UID missing');
}
@@ -315,12 +302,7 @@ export async function createRouter(
allowLimitedAccess: true,
});
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
targetPluginId: 'catalog',
});
const entity = await entityLoader.load(credentials, entityName, token);
const entity = await entityLoader.load(credentials, entityName);
if (!entity) {
throw new NotFoundError(