frontend-plugin-api: convert ApiRef to an opaque type
Convert the ApiRef type in the new frontend system to an opaque type
with a $$type discriminator, matching the pattern used by route refs
and extension data refs. Add a builder-pattern creation overload
(createApiRef<T>().with({ id })) alongside the existing direct-config
form. Create OpaqueApiRef in frontend-internal for internal type
validation.
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
@@ -188,8 +188,9 @@ export type ApiHolder = {
|
||||
|
||||
// @public
|
||||
export type ApiRef<T> = {
|
||||
id: string;
|
||||
T: T;
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
readonly id: string;
|
||||
readonly T: T;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -418,6 +419,11 @@ export function createApiFactory<Api, Impl extends Api>(
|
||||
// @public
|
||||
export function createApiRef<T>(config: ApiRefConfig): ApiRef<T>;
|
||||
|
||||
// @public
|
||||
export function createApiRef<T>(): {
|
||||
with(config: ApiRefConfig): ApiRef<T>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function createExtension<
|
||||
UOutput extends ExtensionDataRef,
|
||||
|
||||
@@ -17,8 +17,17 @@
|
||||
import { createApiRef } from './ApiRef';
|
||||
|
||||
describe('ApiRef', () => {
|
||||
it('should be created', () => {
|
||||
it('should be created with config', () => {
|
||||
const ref = createApiRef({ id: 'abc' });
|
||||
expect(ref.$$type).toBe('@backstage/ApiRef');
|
||||
expect(ref.id).toBe('abc');
|
||||
expect(String(ref)).toBe('apiRef{abc}');
|
||||
expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}');
|
||||
});
|
||||
|
||||
it('should be created with builder pattern', () => {
|
||||
const ref = createApiRef<string>().with({ id: 'abc' });
|
||||
expect(ref.$$type).toBe('@backstage/ApiRef');
|
||||
expect(ref.id).toBe('abc');
|
||||
expect(String(ref)).toBe('apiRef{abc}');
|
||||
expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}');
|
||||
@@ -47,4 +56,10 @@ describe('ApiRef', () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid ids with builder pattern', () => {
|
||||
expect(() => createApiRef().with({ id: '123' })).toThrow(
|
||||
`API id must only contain period separated lowercase alphanum tokens with dashes, got '123'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,48 +25,85 @@ export type ApiRefConfig = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
class ApiRefImpl<T> implements ApiRef<T> {
|
||||
constructor(private readonly config: ApiRefConfig) {
|
||||
const valid = config.id
|
||||
.split('.')
|
||||
.flatMap(part => part.split('-'))
|
||||
.every(part => part.match(/^[a-z][a-z0-9]*$/));
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`,
|
||||
);
|
||||
}
|
||||
function validateId(id: string): void {
|
||||
const valid = id
|
||||
.split('.')
|
||||
.flatMap(part => part.split('-'))
|
||||
.every(part => part.match(/^[a-z][a-z0-9]*$/));
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
// Utility for getting type of an api, using `typeof apiRef.T`
|
||||
get T(): T {
|
||||
throw new Error(`tried to read ApiRef.T of ${this}`);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `apiRef{${this.config.id}}`;
|
||||
}
|
||||
function makeApiRef<T>(id: string): ApiRef<T> {
|
||||
const ref = {
|
||||
$$type: '@backstage/ApiRef' as const,
|
||||
version: 'v1',
|
||||
id,
|
||||
toString() {
|
||||
return `apiRef{${id}}`;
|
||||
},
|
||||
};
|
||||
Object.defineProperty(ref, 'T', {
|
||||
get(): T {
|
||||
throw new Error(`tried to read ApiRef.T of ${this}`);
|
||||
},
|
||||
enumerable: false,
|
||||
});
|
||||
return ref as unknown as ApiRef<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reference to an API. The provided `id` is a stable identifier for
|
||||
* the API implementation.
|
||||
* Creates a reference to an API.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The frontend system infers the owning plugin for an API from the `id`. The
|
||||
* recommended pattern is `plugin.<plugin-id>.*` (for example,
|
||||
* The `id` is a stable identifier for the API implementation. The frontend
|
||||
* system infers the owning plugin for an API from the `id`. The recommended
|
||||
* pattern is `plugin.<plugin-id>.*` (for example,
|
||||
* `plugin.catalog.entity-presentation`). This ensures that other plugins can't
|
||||
* mistakenly override your API implementation.
|
||||
*
|
||||
* @param config - The descriptor of the API to reference.
|
||||
* @returns An API reference.
|
||||
* The recommended way to create an API reference is:
|
||||
*
|
||||
* ```ts
|
||||
* const myApiRef = createApiRef<MyApi>().with({ id: 'plugin.my.api' });
|
||||
* ```
|
||||
*
|
||||
* For backwards compatibility, you can also pass the config directly:
|
||||
*
|
||||
* ```ts
|
||||
* const myApiRef = createApiRef<MyApi>({ id: 'plugin.my.api' });
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createApiRef<T>(config: ApiRefConfig): ApiRef<T> {
|
||||
return new ApiRefImpl<T>(config);
|
||||
export function createApiRef<T>(config: ApiRefConfig): ApiRef<T>;
|
||||
/**
|
||||
* Creates a reference to an API.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Returns a builder with a `.with()` method for providing the `id`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createApiRef<T>(): {
|
||||
with(config: ApiRefConfig): ApiRef<T>;
|
||||
};
|
||||
export function createApiRef<T>(
|
||||
config?: ApiRefConfig,
|
||||
): ApiRef<T> | { with(config: ApiRefConfig): ApiRef<T> } {
|
||||
if (config) {
|
||||
validateId(config.id);
|
||||
return makeApiRef<T>(config.id);
|
||||
}
|
||||
return {
|
||||
with(withConfig: ApiRefConfig): ApiRef<T> {
|
||||
validateId(withConfig.id);
|
||||
return makeApiRef<T>(withConfig.id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
* @public
|
||||
*/
|
||||
export type ApiRef<T> = {
|
||||
id: string;
|
||||
T: T;
|
||||
readonly $$type: '@backstage/ApiRef';
|
||||
readonly id: string;
|
||||
readonly T: T;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user