api-ref: use opaque metadata for owner lookup
Read ApiRef plugin ownership through the internal opaque type helper and gracefully fall back to legacy ID inference for unsupported ref shapes. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
import {
|
||||
AppTreeApi,
|
||||
type ApiRef,
|
||||
appTreeApiRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
@@ -422,6 +423,64 @@ describe('createSpecializedApp', () => {
|
||||
expect(app.apis.get(testApiRef)).toEqual({ value: 'owner' });
|
||||
});
|
||||
|
||||
it('should ignore plugin ownership metadata from unsupported opaque ApiRefs', () => {
|
||||
const testApiRef = {
|
||||
$$type: '@backstage/ApiRef',
|
||||
version: 'v0',
|
||||
id: 'shared.api',
|
||||
pluginId: 'owner',
|
||||
T: null as unknown as { value: string },
|
||||
toString() {
|
||||
return 'apiRef{shared.api}';
|
||||
},
|
||||
} as ApiRef<{ value: string }, 'shared.api'> & {
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
readonly version: 'v0';
|
||||
readonly pluginId: 'owner';
|
||||
};
|
||||
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
makeAppPlugin(),
|
||||
createFrontendPlugin({
|
||||
pluginId: 'other-before',
|
||||
extensions: [
|
||||
ApiBlueprint.make({
|
||||
params: defineParams =>
|
||||
defineParams({
|
||||
api: testApiRef,
|
||||
deps: {},
|
||||
factory: () => ({ value: 'other' }),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createFrontendPlugin({
|
||||
pluginId: 'owner',
|
||||
extensions: [
|
||||
ApiBlueprint.make({
|
||||
params: defineParams =>
|
||||
defineParams({
|
||||
api: testApiRef,
|
||||
deps: {},
|
||||
factory: () => ({ value: 'owner' }),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(app.errors).toEqual([
|
||||
expect.objectContaining({
|
||||
code: 'API_FACTORY_CONFLICT',
|
||||
message: expect.stringContaining("API 'shared.api'"),
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(app.apis.get(testApiRef)).toEqual({ value: 'other' });
|
||||
});
|
||||
|
||||
it('should not infer app ownership from core-prefixed API ids', () => {
|
||||
const testApiRef = createApiRef<{ value: string }>({ id: 'core.shared' });
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
resolveExtensionDefinition,
|
||||
toInternalExtension,
|
||||
} from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { OpaqueApiRef } from '../../../frontend-plugin-api/src/apis/system/ApiRef';
|
||||
|
||||
import {
|
||||
extractRouteInfoFromAppNode,
|
||||
@@ -456,9 +458,15 @@ function createApiFactories(options: {
|
||||
// TODO(Rugvip): It would be good if this was more explicit, but I think that
|
||||
// might need to wait for some future update for API factories.
|
||||
function getApiOwnerId(apiRef: { id: string }): string {
|
||||
const pluginId = (apiRef as { pluginId?: string }).pluginId;
|
||||
if (pluginId) {
|
||||
return pluginId;
|
||||
if (OpaqueApiRef.isType(apiRef)) {
|
||||
try {
|
||||
const { pluginId } = OpaqueApiRef.toInternal(apiRef);
|
||||
if (pluginId) {
|
||||
return pluginId;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to legacy ID inference for unsupported opaque ApiRef versions.
|
||||
}
|
||||
}
|
||||
|
||||
const apiRefId = apiRef.id;
|
||||
|
||||
@@ -31,12 +31,14 @@ type ApiRefBuilderConfig<TId extends string> = {
|
||||
pluginId?: string;
|
||||
};
|
||||
|
||||
const OpaqueApiRef = OpaqueType.create<{
|
||||
/** @internal */
|
||||
export const OpaqueApiRef = OpaqueType.create<{
|
||||
public: ApiRef<unknown> & {
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
};
|
||||
versions: {
|
||||
readonly version: 'v1';
|
||||
readonly pluginId?: string;
|
||||
};
|
||||
}>({
|
||||
type: '@backstage/ApiRef',
|
||||
|
||||
Reference in New Issue
Block a user