catalog-node: add new CatalogService with credentials support
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-node': minor
|
||||
---
|
||||
|
||||
The `catalogServiceRef` now have its own accompanying `CatalogService` interface, which also supports passing Backstage `credentials` objects in addition to tokens.
|
||||
@@ -71,6 +71,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/cli": "workspace:^",
|
||||
"msw": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,37 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AddLocationRequest } from '@backstage/catalog-client';
|
||||
import { AddLocationResponse } from '@backstage/catalog-client';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
|
||||
import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
|
||||
import { CatalogRequestOptions } from '@backstage/catalog-client';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { EntitiesSearchFilter } from '@backstage/plugin-catalog-node';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-node';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { GetEntitiesByRefsRequest } from '@backstage/catalog-client';
|
||||
import { GetEntitiesByRefsResponse } from '@backstage/catalog-client';
|
||||
import { GetEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { GetEntitiesResponse } from '@backstage/catalog-client';
|
||||
import { GetEntityAncestorsRequest } from '@backstage/catalog-client';
|
||||
import { GetEntityAncestorsResponse } from '@backstage/catalog-client';
|
||||
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
|
||||
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
|
||||
import { Location as Location_2 } from '@backstage/catalog-client';
|
||||
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { PermissionRule } from '@backstage/plugin-permission-node';
|
||||
import { PermissionRuleParams } from '@backstage/plugin-permission-common';
|
||||
import { PlaceholderResolver } from '@backstage/plugin-catalog-node';
|
||||
import { QueryEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { QueryEntitiesResponse } from '@backstage/catalog-client';
|
||||
import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-node';
|
||||
import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { ValidateEntityResponse } from '@backstage/catalog-client';
|
||||
import { Validators } from '@backstage/catalog-model';
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -95,8 +112,14 @@ export interface CatalogProcessingExtensionPoint {
|
||||
// @alpha (undocumented)
|
||||
export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "CatalogService" needs to be exported by the entry point alpha.d.ts
|
||||
//
|
||||
// @alpha
|
||||
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin', 'singleton'>;
|
||||
export const catalogServiceRef: ServiceRef<
|
||||
CatalogService,
|
||||
'plugin',
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -14,12 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createBackendModule,
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
ServiceFactoryTester,
|
||||
mockCredentials,
|
||||
mockServices,
|
||||
registerMswTestHooks,
|
||||
startTestBackend,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { catalogServiceRef } from './catalogService';
|
||||
|
||||
describe('catalogServiceRef', () => {
|
||||
const server = setupServer();
|
||||
registerMswTestHooks(server);
|
||||
|
||||
it('should return a catalogClient', async () => {
|
||||
expect.assertions(1);
|
||||
const testModule = createBackendModule({
|
||||
@@ -41,4 +56,119 @@ describe('catalogServiceRef', () => {
|
||||
features: [testModule],
|
||||
});
|
||||
});
|
||||
|
||||
it('should inject token from user credentials', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBe(
|
||||
mockCredentials.service.header({
|
||||
onBehalfOf: mockCredentials.user(),
|
||||
targetPluginId: 'catalog',
|
||||
}),
|
||||
);
|
||||
return res(ctx.json({}));
|
||||
}),
|
||||
);
|
||||
const tester = ServiceFactoryTester.from(
|
||||
createServiceFactory({
|
||||
service: createServiceRef<void>({ id: 'unused-dummy' }),
|
||||
deps: {},
|
||||
factory() {},
|
||||
}),
|
||||
{ dependencies: [mockServices.discovery.factory()] },
|
||||
);
|
||||
|
||||
const catalogService = await tester.getService(catalogServiceRef);
|
||||
|
||||
await catalogService.getEntities(
|
||||
{},
|
||||
{ credentials: mockCredentials.user() },
|
||||
);
|
||||
});
|
||||
|
||||
it('should inject token from service credentials', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBe(
|
||||
mockCredentials.service.header({
|
||||
onBehalfOf: mockCredentials.service(),
|
||||
targetPluginId: 'catalog',
|
||||
}),
|
||||
);
|
||||
return res(ctx.json({}));
|
||||
}),
|
||||
);
|
||||
const tester = ServiceFactoryTester.from(
|
||||
createServiceFactory({
|
||||
service: createServiceRef<void>({ id: 'unused-dummy' }),
|
||||
deps: {},
|
||||
factory() {},
|
||||
}),
|
||||
{ dependencies: [mockServices.discovery.factory()] },
|
||||
);
|
||||
|
||||
const catalogService = await tester.getService(catalogServiceRef);
|
||||
|
||||
await catalogService.getEntities(
|
||||
{},
|
||||
{ credentials: mockCredentials.service() },
|
||||
);
|
||||
});
|
||||
|
||||
it('should call with token', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBe(
|
||||
mockCredentials.user.header(),
|
||||
);
|
||||
return res(ctx.json({}));
|
||||
}),
|
||||
);
|
||||
const tester = ServiceFactoryTester.from(
|
||||
createServiceFactory({
|
||||
service: createServiceRef<void>({ id: 'unused-dummy' }),
|
||||
deps: {},
|
||||
factory() {},
|
||||
}),
|
||||
{ dependencies: [mockServices.discovery.factory()] },
|
||||
);
|
||||
|
||||
const catalogService = await tester.getService(catalogServiceRef);
|
||||
|
||||
await catalogService.getEntities(
|
||||
{},
|
||||
{
|
||||
token: mockCredentials.user.token(),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should call without credentials', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
server.use(
|
||||
rest.get('http://localhost/api/catalog/entities', (req, res, ctx) => {
|
||||
expect(req.headers.get('authorization')).toBeFalsy();
|
||||
return res(ctx.json({}));
|
||||
}),
|
||||
);
|
||||
const tester = ServiceFactoryTester.from(
|
||||
createServiceFactory({
|
||||
service: createServiceRef<void>({ id: 'unused-dummy' }),
|
||||
deps: {},
|
||||
factory() {},
|
||||
}),
|
||||
{ dependencies: [mockServices.discovery.factory()] },
|
||||
);
|
||||
|
||||
const catalogService = await tester.getService(catalogServiceRef);
|
||||
|
||||
await catalogService.getEntities();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,23 +18,269 @@ import {
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
coreServices,
|
||||
BackstageCredentials,
|
||||
DiscoveryService,
|
||||
AuthService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogClient,
|
||||
CatalogRequestOptions,
|
||||
GetEntitiesByRefsRequest,
|
||||
GetEntitiesByRefsResponse,
|
||||
GetEntitiesRequest,
|
||||
GetEntitiesResponse,
|
||||
GetEntityAncestorsRequest,
|
||||
GetEntityAncestorsResponse,
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
Location,
|
||||
QueryEntitiesRequest,
|
||||
QueryEntitiesResponse,
|
||||
ValidateEntityResponse,
|
||||
} from '@backstage/catalog-client';
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface CatalogServiceRequestOptions extends CatalogRequestOptions {
|
||||
credentials?: BackstageCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of the {@link CatalogApi | CatalogApi} that
|
||||
* accepts backend credentials in addition to a token.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CatalogService extends CatalogApi {
|
||||
getEntities(
|
||||
request?: GetEntitiesRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntitiesResponse>;
|
||||
|
||||
getEntitiesByRefs(
|
||||
request: GetEntitiesByRefsRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntitiesByRefsResponse>;
|
||||
|
||||
queryEntities(
|
||||
request?: QueryEntitiesRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<QueryEntitiesResponse>;
|
||||
|
||||
getEntityAncestors(
|
||||
request: GetEntityAncestorsRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntityAncestorsResponse>;
|
||||
|
||||
getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Entity | undefined>;
|
||||
|
||||
removeEntityByUid(
|
||||
uid: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
refreshEntity(
|
||||
entityRef: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
getEntityFacets(
|
||||
request: GetEntityFacetsRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntityFacetsResponse>;
|
||||
|
||||
getLocationById(
|
||||
id: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined>;
|
||||
|
||||
getLocationByRef(
|
||||
locationRef: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined>;
|
||||
|
||||
addLocation(
|
||||
location: AddLocationRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<AddLocationResponse>;
|
||||
|
||||
removeLocationById(
|
||||
id: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined>;
|
||||
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
locationRef: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<ValidateEntityResponse>;
|
||||
}
|
||||
|
||||
class DefaultCatalogService extends CatalogClient {
|
||||
readonly #auth: AuthService;
|
||||
|
||||
constructor({
|
||||
discoveryApi,
|
||||
auth,
|
||||
}: {
|
||||
discoveryApi: DiscoveryService;
|
||||
auth: AuthService;
|
||||
}) {
|
||||
super({ discoveryApi });
|
||||
this.#auth = auth;
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
request?: GetEntitiesRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntitiesResponse> {
|
||||
return super.getEntities(request, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getEntitiesByRefs(
|
||||
request: GetEntitiesByRefsRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntitiesByRefsResponse> {
|
||||
return super.getEntitiesByRefs(request, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async queryEntities(
|
||||
request?: QueryEntitiesRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
return super.queryEntities(request, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getEntityAncestors(
|
||||
request: GetEntityAncestorsRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntityAncestorsResponse> {
|
||||
return super.getEntityAncestors(request, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Entity | undefined> {
|
||||
return super.getEntityByRef(entityRef, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async removeEntityByUid(
|
||||
uid: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<void> {
|
||||
return super.removeEntityByUid(uid, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async refreshEntity(
|
||||
entityRef: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<void> {
|
||||
return super.refreshEntity(entityRef, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getEntityFacets(
|
||||
request: GetEntityFacetsRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<GetEntityFacetsResponse> {
|
||||
return super.getEntityFacets(request, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getLocationById(
|
||||
id: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return super.getLocationById(id, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getLocationByRef(
|
||||
locationRef: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return super.getLocationByRef(locationRef, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async addLocation(
|
||||
location: AddLocationRequest,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<AddLocationResponse> {
|
||||
return super.addLocation(location, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async removeLocationById(
|
||||
id: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<void> {
|
||||
return super.removeLocationById(id, await this.#getOptions(options));
|
||||
}
|
||||
|
||||
async getLocationByEntity(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<Location | undefined> {
|
||||
return super.getLocationByEntity(
|
||||
entityRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async validateEntity(
|
||||
entity: Entity,
|
||||
locationRef: string,
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<ValidateEntityResponse> {
|
||||
return super.validateEntity(
|
||||
entity,
|
||||
locationRef,
|
||||
await this.#getOptions(options),
|
||||
);
|
||||
}
|
||||
|
||||
async #getOptions(
|
||||
options?: CatalogServiceRequestOptions,
|
||||
): Promise<CatalogRequestOptions | undefined> {
|
||||
if (options?.token) {
|
||||
return options;
|
||||
}
|
||||
if (options?.credentials) {
|
||||
return this.#auth.getPluginRequestToken({
|
||||
onBehalfOf: options.credentials,
|
||||
targetPluginId: 'catalog',
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalogService provides the catalog API.
|
||||
* @alpha
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const catalogServiceRef = createServiceRef<CatalogApi>({
|
||||
export const catalogServiceRef = createServiceRef<CatalogService>({
|
||||
id: 'catalog-client',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
auth: coreServices.auth,
|
||||
discoveryApi: coreServices.discovery,
|
||||
},
|
||||
async factory({ discoveryApi }) {
|
||||
return new CatalogClient({ discoveryApi });
|
||||
async factory(deps) {
|
||||
return new DefaultCatalogService(deps);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user